diff --git a/docs/src/headers/mutex/shared_mutex.md b/docs/src/headers/mutex/shared_mutex.md index 64662e3..4cbc9c4 100644 --- a/docs/src/headers/mutex/shared_mutex.md +++ b/docs/src/headers/mutex/shared_mutex.md @@ -19,13 +19,18 @@ struct shared_mutex { void unlock(); // (3) void unlock_shared(); // (4) + + void downgrade(); // (5) }; ``` 1. Asynchronously acquire the mutex in exclusive mode. -1. Asynchronously acquire the mutex in shared mode. +2. Asynchronously acquire the mutex in shared mode. 3. Release the mutex (mutex must be in exclusive mode). -3. Release the mutex (mutex must be in shared mode). +4. Release the mutex (mutex must be in shared mode). +5. Convert an exclusive lock into a shared one (mutex must be in exclusive mode). +Ownership of the mutex is never given up, hence no other exclusive owner can acquire the mutex in between. +Since the lock is shared after this call, it must be released by `unlock_shared()`. ### Return values 1. This method returns a sender of unspecified type. The sender does not return @@ -33,6 +38,7 @@ any value, and completes once the mutex is acquired. 2. Same as (1). 3. This method doesn't return any value. 4. Same as (3). +5. Same as (3). ## Examples diff --git a/include/async/mutex.hpp b/include/async/mutex.hpp index abc84ad..50c2087 100644 --- a/include/async/mutex.hpp +++ b/include/async/mutex.hpp @@ -217,6 +217,15 @@ namespace detail { bool exclusive; }; + using node_list = frg::intrusive_list< + node, + frg::locate_member< + node, + frg::default_list_hook, + &node::hook + > + >; + public: shared_mutex() = default; @@ -464,14 +473,7 @@ namespace detail { // Only the owner ever transitions out of state::locked so we must be in state::contended. assert(st.c == contention::contended); - frg::intrusive_list< - node, - frg::locate_member< - node, - frg::default_list_hook, - &node::hook - > - > pending; + node_list pending; { frg::unique_lock lock(mutex_); @@ -593,6 +595,63 @@ namespace detail { next->complete(); } + // Atomically converts an exclusive lock into a shared one. + void downgrade() { + auto st = st_.load(std::memory_order_relaxed); + assert(st.c != contention::none); + assert(!st.shared_cnt); + + // If there is no contention, we can downgrade without taking mutex_. + if (st.c == contention::locked) { + bool success = st_.compare_exchange_strong( + st, + state{.c = contention::locked, .shared_cnt = 1}, + std::memory_order_release, + std::memory_order_relaxed + ); + if (success) + return; + assert(!st.shared_cnt); + } + // Only the owner ever transitions out of state::locked so we must be in state::contended. + assert(st.c == contention::contended); + + // Shared waiters can share the lock with us, hence we need to wake them. + // This also maintains the invariant that the first waiter is exclusive + // whenever the shared count is non-zero. + node_list pending; + { + frg::unique_lock lock(mutex_); + + // Otherwise, we would not be in state::contended. + assert(!waiters_.empty()); + + // We keep holding the lock in shared mode. + unsigned int n = 1; + while (!waiters_.empty() && !waiters_.front()->exclusive) { + pending.push_back(waiters_.pop_front()); + ++n; + } + if (waiters_.empty()) { + // Release since state::locked with a non-zero shared count can be joined + // by try_lock_shared() without taking mutex_. + st_.store( + state{.c = contention::locked, .shared_cnt = n}, + std::memory_order_release + ); + } else { + // Hand-off to a waiter does not require a fence. + st_.store( + state{.c = contention::contended, .shared_cnt = n}, + std::memory_order_relaxed + ); + } + } + + while (!pending.empty()) + pending.pop_front()->complete(); + } + private: platform::mutex mutex_; @@ -603,14 +662,7 @@ namespace detail { // which can happen outside of mutex_. std::atomic st_{state{.c = contention::none, .shared_cnt = 0}}; - frg::intrusive_list< - node, - frg::locate_member< - node, - frg::default_list_hook, - &node::hook - > - > waiters_; + node_list waiters_; }; } diff --git a/tests/mutex.cpp b/tests/mutex.cpp index 8fff9af..4cb457f 100644 --- a/tests/mutex.cpp +++ b/tests/mutex.cpp @@ -1,9 +1,25 @@ #include +#include +#include #include #include #include +namespace { + +async::detached lockAndRecord(async::shared_mutex *mtx, std::vector *order, int id) { + co_await mtx->async_lock(); + order->push_back(id); +} + +async::detached lockSharedAndRecord(async::shared_mutex *mtx, std::vector *order, int id) { + co_await mtx->async_lock_shared(); + order->push_back(id); +} + +} // anonymous namespace + TEST(Mutex, TryLock) { async::mutex m; @@ -13,3 +29,63 @@ TEST(Mutex, TryLock) { ASSERT_TRUE(m.try_lock()); m.unlock(); } + +TEST(SharedMutex, Downgrade) { + async::shared_mutex mtx; + + ASSERT_TRUE(mtx.try_lock()); + mtx.downgrade(); + + // The mutex is now held in shared mode. + ASSERT_FALSE(mtx.try_lock()); + ASSERT_TRUE(mtx.try_lock_shared()); + mtx.unlock_shared(); + + mtx.unlock_shared(); + ASSERT_TRUE(mtx.try_lock()); + mtx.unlock(); +} + +TEST(SharedMutex, DowngradeWakesSharedWaiters) { + async::shared_mutex mtx; + std::vector order; + + ASSERT_TRUE(mtx.try_lock()); + lockSharedAndRecord(&mtx, &order, 1); + lockSharedAndRecord(&mtx, &order, 2); + lockAndRecord(&mtx, &order, 3); + ASSERT_TRUE(order.empty()); + + // Both shared waiters can share the lock with us. + mtx.downgrade(); + ASSERT_EQ(order, (std::vector{1, 2})); + + // The exclusive waiter has to wait for all three shared owners. + mtx.unlock_shared(); + mtx.unlock_shared(); + ASSERT_EQ(order, (std::vector{1, 2})); + mtx.unlock_shared(); + ASSERT_EQ(order, (std::vector{1, 2, 3})); + mtx.unlock(); +} + +TEST(SharedMutex, DowngradeBehindExclusiveWaiter) { + async::shared_mutex mtx; + std::vector order; + + ASSERT_TRUE(mtx.try_lock()); + lockAndRecord(&mtx, &order, 1); + lockSharedAndRecord(&mtx, &order, 2); + + // The shared waiter must not overtake the exclusive waiter, + // hence downgrading does not wake any waiter here. + mtx.downgrade(); + ASSERT_TRUE(order.empty()); + ASSERT_FALSE(mtx.try_lock_shared()); + + mtx.unlock_shared(); + ASSERT_EQ(order, (std::vector{1})); + mtx.unlock(); + ASSERT_EQ(order, (std::vector{1, 2})); + mtx.unlock_shared(); +}