From 5d6d6bde641ee6ef6eca0f316ca08c11b304671a Mon Sep 17 00:00:00 2001 From: Alexander van der Grinten Date: Sat, 29 Aug 2026 08:25:09 +0200 Subject: [PATCH 1/4] shared_mutex: Add an alias for the waiter list type Assisted-by: Claude Code (Opus 5) --- include/async/mutex.hpp | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/include/async/mutex.hpp b/include/async/mutex.hpp index abc84ad..b0f0860 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_); @@ -603,14 +605,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_; }; } From 81341275cd244755b136f21b2b9abf697072a9b3 Mon Sep 17 00:00:00 2001 From: Alexander van der Grinten Date: Sat, 29 Aug 2026 08:26:39 +0200 Subject: [PATCH 2/4] shared_mutex: Implement downgrade() Assisted-by: Claude Code (Opus 5) --- include/async/mutex.hpp | 57 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/include/async/mutex.hpp b/include/async/mutex.hpp index b0f0860..50c2087 100644 --- a/include/async/mutex.hpp +++ b/include/async/mutex.hpp @@ -595,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_; From 05bd831dd05ceb391e29b554653bf4d9430dbef8 Mon Sep 17 00:00:00 2001 From: Alexander van der Grinten Date: Sat, 29 Aug 2026 08:26:39 +0200 Subject: [PATCH 3/4] tests: Add shared_mutex downgrade tests Assisted-by: Claude Code (Opus 5) --- tests/mutex.cpp | 76 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) 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(); +} From c7f0847f2283ebcf123eb19bc49c5b205c366bfe Mon Sep 17 00:00:00 2001 From: Alexander van der Grinten Date: Sat, 29 Aug 2026 08:26:57 +0200 Subject: [PATCH 4/4] docs: Document shared_mutex::downgrade() Assisted-by: Claude Code (Opus 5) --- docs/src/headers/mutex/shared_mutex.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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