diff --git a/src/common-tests/threading_tests.cpp b/src/common-tests/threading_tests.cpp index b51a4eba5..f527f75e5 100644 --- a/src/common-tests/threading_tests.cpp +++ b/src/common-tests/threading_tests.cpp @@ -7,12 +7,15 @@ #include #include +#include #include #include #include static_assert(!std::is_copy_constructible_v); static_assert(!std::is_copy_assignable_v); +static_assert(!std::is_copy_constructible_v); +static_assert(!std::is_copy_assignable_v); static_assert(!std::is_copy_constructible_v); static_assert(!std::is_copy_assignable_v); @@ -74,6 +77,83 @@ TEST(ThreadingMutex, MutualExclusion) EXPECT_EQ(value, NUM_THREADS * NUM_INCREMENTS); } +TEST(ThreadingSharedMutex, TryLock) +{ + Threading::SharedMutex mutex; + EXPECT_TRUE(mutex.try_lock()); + + bool exclusive_locked = true; + bool shared_locked = true; + std::thread exclusive_test_thread([&]() { + exclusive_locked = mutex.try_lock(); + shared_locked = mutex.try_lock_shared(); + }); + exclusive_test_thread.join(); + EXPECT_FALSE(exclusive_locked); + EXPECT_FALSE(shared_locked); + mutex.unlock(); + + EXPECT_TRUE(mutex.try_lock_shared()); + std::thread shared_test_thread([&]() { + shared_locked = mutex.try_lock_shared(); + if (shared_locked) + mutex.unlock_shared(); + exclusive_locked = mutex.try_lock(); + }); + shared_test_thread.join(); + EXPECT_TRUE(shared_locked); + EXPECT_FALSE(exclusive_locked); + mutex.unlock_shared(); + + EXPECT_TRUE(mutex.try_lock()); + mutex.unlock(); +} + +TEST(ThreadingSharedMutex, StandardLockWrappers) +{ + Threading::SharedMutex mutex; + + { + const std::lock_guard lock(mutex); + } + + { + std::unique_lock lock(mutex); + EXPECT_TRUE(lock.owns_lock()); + } + + { + std::shared_lock lock(mutex); + EXPECT_TRUE(lock.owns_lock()); + } +} + +TEST(ThreadingSharedMutex, MutualExclusion) +{ + static constexpr u32 NUM_THREADS = 4; + static constexpr u32 NUM_INCREMENTS = 10000; + + Threading::SharedMutex mutex; + u32 value = 0; + std::vector threads; + threads.reserve(NUM_THREADS); + for (u32 i = 0; i < NUM_THREADS; i++) + { + threads.emplace_back([&mutex, &value]() { + for (u32 j = 0; j < NUM_INCREMENTS; j++) + { + const std::lock_guard lock(mutex); + value++; + } + }); + } + + for (std::thread& thread : threads) + thread.join(); + + EXPECT_EQ(value, NUM_THREADS * NUM_INCREMENTS); +} + TEST(ThreadingConditionVariable, NotifyOneAndPredicateWait) { Threading::Mutex mutex; diff --git a/src/common/threading.cpp b/src/common/threading.cpp index af3fa7cad..1cd7d2987 100644 --- a/src/common/threading.cpp +++ b/src/common/threading.cpp @@ -55,9 +55,11 @@ LOG_CHANNEL(Threading); #ifdef _WIN32 #define NATIVE_MUTEX_PTR(storage) static_cast(static_cast(&(storage))) +#define NATIVE_SHARED_MUTEX_PTR(storage) static_cast(static_cast(&(storage))) #define NATIVE_CONDITION_VARIABLE_PTR(storage) static_cast(static_cast(&(storage))) #else #define NATIVE_MUTEX_PTR(storage) static_cast(static_cast(&(storage))) +#define NATIVE_SHARED_MUTEX_PTR(storage) static_cast(static_cast(&(storage))) #define NATIVE_CONDITION_VARIABLE_PTR(storage) static_cast(static_cast(&(storage))) #endif @@ -674,6 +676,86 @@ void Threading::Mutex::unlock() #ifndef _WIN32 +Threading::SharedMutex::SharedMutex() +{ + static_assert(sizeof(m_data) == sizeof(pthread_rwlock_t)); + static_assert(alignof(SharedMutex) >= alignof(pthread_rwlock_t)); + [[maybe_unused]] const int result = pthread_rwlock_init(NATIVE_SHARED_MUTEX_PTR(m_data), nullptr); + DebugAssert(result == 0); +} + +Threading::SharedMutex::~SharedMutex() +{ + [[maybe_unused]] const int result = pthread_rwlock_destroy(NATIVE_SHARED_MUTEX_PTR(m_data)); + DebugAssert(result == 0); +} + +#endif // _WIN32 + +void Threading::SharedMutex::lock() +{ +#ifdef _WIN32 + AcquireSRWLockExclusive(NATIVE_SHARED_MUTEX_PTR(m_data)); +#else + [[maybe_unused]] const int result = pthread_rwlock_wrlock(NATIVE_SHARED_MUTEX_PTR(m_data)); + DebugAssert(result == 0); +#endif +} + +bool Threading::SharedMutex::try_lock() +{ +#ifdef _WIN32 + return TryAcquireSRWLockExclusive(NATIVE_SHARED_MUTEX_PTR(m_data)) != FALSE; +#else + const int result = pthread_rwlock_trywrlock(NATIVE_SHARED_MUTEX_PTR(m_data)); + DebugAssert(result == 0 || result == EBUSY); + return result == 0; +#endif +} + +void Threading::SharedMutex::unlock() +{ +#ifdef _WIN32 + ReleaseSRWLockExclusive(NATIVE_SHARED_MUTEX_PTR(m_data)); +#else + [[maybe_unused]] const int result = pthread_rwlock_unlock(NATIVE_SHARED_MUTEX_PTR(m_data)); + DebugAssert(result == 0); +#endif +} + +void Threading::SharedMutex::lock_shared() +{ +#ifdef _WIN32 + AcquireSRWLockShared(NATIVE_SHARED_MUTEX_PTR(m_data)); +#else + [[maybe_unused]] const int result = pthread_rwlock_rdlock(NATIVE_SHARED_MUTEX_PTR(m_data)); + DebugAssert(result == 0); +#endif +} + +bool Threading::SharedMutex::try_lock_shared() +{ +#ifdef _WIN32 + return TryAcquireSRWLockShared(NATIVE_SHARED_MUTEX_PTR(m_data)) != FALSE; +#else + const int result = pthread_rwlock_tryrdlock(NATIVE_SHARED_MUTEX_PTR(m_data)); + DebugAssert(result == 0 || result == EBUSY || result == EAGAIN); + return result == 0; +#endif +} + +void Threading::SharedMutex::unlock_shared() +{ +#ifdef _WIN32 + ReleaseSRWLockShared(NATIVE_SHARED_MUTEX_PTR(m_data)); +#else + [[maybe_unused]] const int result = pthread_rwlock_unlock(NATIVE_SHARED_MUTEX_PTR(m_data)); + DebugAssert(result == 0); +#endif +} + +#ifndef _WIN32 + Threading::ConditionVariable::ConditionVariable() { static_assert(sizeof(m_data) == sizeof(pthread_cond_t)); @@ -937,6 +1019,7 @@ bool Threading::KernelSemaphore::TryWait() } #undef NATIVE_MUTEX_PTR +#undef NATIVE_SHARED_MUTEX_PTR #undef NATIVE_CONDITION_VARIABLE_PTR #if !defined(_WIN32) #undef NATIVE_SEMAPHORE diff --git a/src/common/threading.h b/src/common/threading.h index ae01421e2..090cf3aa3 100644 --- a/src/common/threading.h +++ b/src/common/threading.h @@ -165,6 +165,51 @@ private: #endif }; +// -------------------------------------------------------------------------------------- +// SharedMutex +// -------------------------------------------------------------------------------------- +// A lightweight replacement for std::shared_mutex. The native object is stored inline +// without exposing platform headers to users of this header. +// +class SharedMutex +{ +public: +#ifdef _WIN32 + SharedMutex() = default; +#else + SharedMutex(); + ~SharedMutex(); +#endif + + SharedMutex(const SharedMutex&) = delete; + SharedMutex& operator=(const SharedMutex&) = delete; + + void lock(); + bool try_lock(); + void unlock(); + + void lock_shared(); + bool try_lock_shared(); + void unlock_shared(); + +private: +#if defined(_WIN32) + void* m_data = nullptr; +#elif defined(__APPLE__) + static constexpr u32 NATIVE_STORAGE_SIZE = 200; +#elif defined(__ANDROID__) + static constexpr u32 NATIVE_STORAGE_SIZE = (sizeof(void*) == 8) ? 56 : 40; +#elif defined(__linux__) + static constexpr u32 NATIVE_STORAGE_SIZE = (sizeof(void*) == 8) ? 56 : 32; +#else +#error Unsupported platform. +#endif + +#if !defined(_WIN32) + alignas(void*) u8 m_data[NATIVE_STORAGE_SIZE]; +#endif +}; + // -------------------------------------------------------------------------------------- // ConditionVariable // --------------------------------------------------------------------------------------