100.00% Lines (91/91) 100.00% Functions (20/20)
TLA Baseline Branch
Line Hits Code Line Hits Code
1   // 1   //
2   // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) 2   // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
  3 + // Copyright (c) 2026 Michael Vandeberg
3   // 4   //
4   // Distributed under the Boost Software License, Version 1.0. (See accompanying 5   // Distributed under the Boost Software License, Version 1.0. (See accompanying
5   // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) 6   // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6   // 7   //
7   // Official repository: https://github.com/cppalliance/capy 8   // Official repository: https://github.com/cppalliance/capy
8   // 9   //
9   10  
10   #ifndef BOOST_CAPY_ASYNC_MUTEX_HPP 11   #ifndef BOOST_CAPY_ASYNC_MUTEX_HPP
11   #define BOOST_CAPY_ASYNC_MUTEX_HPP 12   #define BOOST_CAPY_ASYNC_MUTEX_HPP
12   13  
13   #include <boost/capy/detail/config.hpp> 14   #include <boost/capy/detail/config.hpp>
14   #include <boost/capy/detail/intrusive.hpp> 15   #include <boost/capy/detail/intrusive.hpp>
15   #include <boost/capy/continuation.hpp> 16   #include <boost/capy/continuation.hpp>
16   #include <boost/capy/concept/executor.hpp> 17   #include <boost/capy/concept/executor.hpp>
17   #include <boost/capy/error.hpp> 18   #include <boost/capy/error.hpp>
18   #include <boost/capy/ex/io_env.hpp> 19   #include <boost/capy/ex/io_env.hpp>
19   #include <boost/capy/io_result.hpp> 20   #include <boost/capy/io_result.hpp>
20   21  
21   #include <stop_token> 22   #include <stop_token>
22   23  
23   #include <atomic> 24   #include <atomic>
24   #include <coroutine> 25   #include <coroutine>
25   #include <new> 26   #include <new>
26   #include <utility> 27   #include <utility>
27   28  
28   /* async_mutex implementation notes 29   /* async_mutex implementation notes
29   ================================ 30   ================================
30   31  
31   Waiters form a doubly-linked intrusive list (fair FIFO). lock_awaiter 32   Waiters form a doubly-linked intrusive list (fair FIFO). lock_awaiter
32   inherits intrusive_list<lock_awaiter>::node; the list is owned by 33   inherits intrusive_list<lock_awaiter>::node; the list is owned by
33   async_mutex::waiters_. 34   async_mutex::waiters_.
34   35  
35   Cancellation via stop_token 36   Cancellation via stop_token
36   --------------------------- 37   ---------------------------
37   A std::stop_callback is registered in await_suspend. Two actors can 38   A std::stop_callback is registered in await_suspend. Two actors can
38   race to resume the suspended coroutine: unlock() and the stop callback. 39   race to resume the suspended coroutine: unlock() and the stop callback.
39   An atomic bool `claimed_` resolves the race -- whoever does 40   An atomic bool `claimed_` resolves the race -- whoever does
40   claimed_.exchange(true) and reads false wins. The loser does nothing. 41   claimed_.exchange(true) and reads false wins. The loser does nothing.
41   42  
42   The stop callback calls ex_.post(h_). The stop_callback is 43   The stop callback calls ex_.post(h_). The stop_callback is
43   destroyed later in await_resume. cancel_fn touches no members 44   destroyed later in await_resume. cancel_fn touches no members
44   after post returns (same pattern as delete-this). 45   after post returns (same pattern as delete-this).
45   46  
46   unlock() pops waiters from the front. If the popped waiter was 47   unlock() pops waiters from the front. If the popped waiter was
47   already claimed by the stop callback, unlock() skips it and tries 48   already claimed by the stop callback, unlock() skips it and tries
48   the next. await_resume removes the (still-linked) canceled waiter 49   the next. await_resume removes the (still-linked) canceled waiter
49   via waiters_.remove(this). 50   via waiters_.remove(this).
50   51  
51   The stop_callback lives in a union to suppress automatic 52   The stop_callback lives in a union to suppress automatic
52   construction/destruction. Placement new in await_suspend, explicit 53   construction/destruction. Placement new in await_suspend, explicit
53   destructor call in await_resume and ~lock_awaiter. 54   destructor call in await_resume and ~lock_awaiter.
54   55  
55   Member ordering constraint 56   Member ordering constraint
56   -------------------------- 57   --------------------------
57   The union containing stop_cb_ must be declared AFTER the members 58   The union containing stop_cb_ must be declared AFTER the members
58   the callback accesses (h_, ex_, claimed_, canceled_). If the 59   the callback accesses (h_, ex_, claimed_, canceled_). If the
59   stop_cb_ destructor blocks waiting for a concurrent callback, those 60   stop_cb_ destructor blocks waiting for a concurrent callback, those
60   members must still be alive (C++ destroys in reverse declaration 61   members must still be alive (C++ destroys in reverse declaration
61   order). 62   order).
62   63  
63   active_ flag 64   active_ flag
64   ------------ 65   ------------
65   Tracks both list membership and stop_cb_ lifetime (they are always 66   Tracks both list membership and stop_cb_ lifetime (they are always
66   set and cleared together). Used by the destructor to clean up if the 67   set and cleared together). Used by the destructor to clean up if the
67   coroutine is destroyed while suspended (e.g. execution_context 68   coroutine is destroyed while suspended (e.g. execution_context
68   shutdown). 69   shutdown).
69   70  
70   Cancellation scope 71   Cancellation scope
71   ------------------ 72   ------------------
72   Cancellation only takes effect while the coroutine is suspended in 73   Cancellation only takes effect while the coroutine is suspended in
73   the wait queue. If the mutex is unlocked, await_ready acquires it 74   the wait queue. If the mutex is unlocked, await_ready acquires it
74   immediately without checking the stop token. This is intentional: 75   immediately without checking the stop token. This is intentional:
75   the fast path has no token access and no overhead. 76   the fast path has no token access and no overhead.
76   77  
77   Threading assumptions 78   Threading assumptions
78   --------------------- 79   ---------------------
79   - All list mutations happen on the executor thread (await_suspend, 80   - All list mutations happen on the executor thread (await_suspend,
80   await_resume, unlock, ~lock_awaiter). 81   await_resume, unlock, ~lock_awaiter).
81   - The stop callback may fire from any thread, but only touches 82   - The stop callback may fire from any thread, but only touches
82   claimed_ (atomic) and then calls post. It never touches the 83   claimed_ (atomic) and then calls post. It never touches the
83   list. 84   list.
84   - ~lock_awaiter must be called from the executor thread. This is 85   - ~lock_awaiter must be called from the executor thread. This is
85   guaranteed during normal shutdown but NOT if the coroutine frame 86   guaranteed during normal shutdown but NOT if the coroutine frame
86   is destroyed from another thread while a stop callback could 87   is destroyed from another thread while a stop callback could
87   fire (precondition violation, same as cppcoro/folly). 88   fire (precondition violation, same as cppcoro/folly).
88   */ 89   */
89   90  
90   namespace boost { 91   namespace boost {
91   namespace capy { 92   namespace capy {
92   93  
93 - /** An asynchronous mutex for coroutines. 94 + /** Queues coroutines in `lock()` and resumes exactly one when the mutex is free.
94   95  
95   This mutex provides mutual exclusion for coroutines without blocking. 96   This mutex provides mutual exclusion for coroutines without blocking.
96   When a coroutine attempts to acquire a locked mutex, it suspends and 97   When a coroutine attempts to acquire a locked mutex, it suspends and
97   is added to an intrusive wait queue. When the holder unlocks, the next 98   is added to an intrusive wait queue. When the holder unlocks, the next
98   waiter is resumed with the lock held. 99   waiter is resumed with the lock held.
99   100  
100   @par Cancellation 101   @par Cancellation
101   102  
102   When a coroutine is suspended waiting for the mutex and its stop 103   When a coroutine is suspended waiting for the mutex and its stop
103   token is triggered, the waiter completes with `error::canceled` 104   token is triggered, the waiter completes with `error::canceled`
104   instead of acquiring the lock. 105   instead of acquiring the lock.
105   106  
106   Cancellation only applies while the coroutine is suspended in the 107   Cancellation only applies while the coroutine is suspended in the
107   wait queue. If the mutex is unlocked when `lock()` is called, the 108   wait queue. If the mutex is unlocked when `lock()` is called, the
108   lock is acquired immediately even if the stop token is already 109   lock is acquired immediately even if the stop token is already
109   signaled. 110   signaled.
110   111  
111   @par Zero Allocation 112   @par Zero Allocation
112   113  
113   No heap allocation occurs for lock operations. 114   No heap allocation occurs for lock operations.
114   115  
115   @par Thread Safety 116   @par Thread Safety
116   117  
117   Distinct objects: Safe.@n 118   Distinct objects: Safe.@n
118   Shared objects: Unsafe. 119   Shared objects: Unsafe.
119   120  
120   The mutex operations are designed for single-threaded use on one 121   The mutex operations are designed for single-threaded use on one
121   executor. The stop callback may fire from any thread. 122   executor. The stop callback may fire from any thread.
122   123  
123   This type is non-copyable and non-movable because suspended 124   This type is non-copyable and non-movable because suspended
124   waiters hold intrusive pointers into the mutex's internal list. 125   waiters hold intrusive pointers into the mutex's internal list.
125   126  
126   @par Example 127   @par Example
127   @code 128   @code
128   async_mutex cm; 129   async_mutex cm;
129   130  
130   task<> protected_operation() { 131   task<> protected_operation() {
131   auto [ec] = co_await cm.lock(); 132   auto [ec] = co_await cm.lock();
132   if(ec) 133   if(ec)
133   co_return; 134   co_return;
134   // ... critical section ... 135   // ... critical section ...
135   cm.unlock(); 136   cm.unlock();
136   } 137   }
137   138  
138   // Or with RAII: 139   // Or with RAII:
139 - task<> protected_operation() { 140 + task<> protected_operation_raii() {
140   auto [ec, guard] = co_await cm.scoped_lock(); 141   auto [ec, guard] = co_await cm.scoped_lock();
141   if(ec) 142   if(ec)
142   co_return; 143   co_return;
143   // ... critical section ... 144   // ... critical section ...
144   // unlocks automatically 145   // unlocks automatically
145   } 146   }
146   @endcode 147   @endcode
147   */ 148   */
148   class async_mutex 149   class async_mutex
149   { 150   {
150   public: 151   public:
151   class lock_awaiter; 152   class lock_awaiter;
152   class lock_guard; 153   class lock_guard;
153   class lock_guard_awaiter; 154   class lock_guard_awaiter;
154   155  
155   private: 156   private:
156   bool locked_ = false; 157   bool locked_ = false;
157   detail::intrusive_list<lock_awaiter> waiters_; 158   detail::intrusive_list<lock_awaiter> waiters_;
158   159  
159   public: 160   public:
160 - /** Awaiter returned by lock(). 161 + /** Suspends the caller until the mutex is free, or resumes it with `error::canceled` on a stop request.
161   */ 162   */
162   class lock_awaiter 163   class lock_awaiter
163   : public detail::intrusive_list<lock_awaiter>::node 164   : public detail::intrusive_list<lock_awaiter>::node
164   { 165   {
165   friend class async_mutex; 166   friend class async_mutex;
166   167  
167   async_mutex* m_; 168   async_mutex* m_;
168   continuation cont_; 169   continuation cont_;
169   executor_ref ex_; 170   executor_ref ex_;
170   171  
171   // These members must be declared before stop_cb_ 172   // These members must be declared before stop_cb_
172   // (see comment on the union below). 173   // (see comment on the union below).
173   std::atomic<bool> claimed_{false}; 174   std::atomic<bool> claimed_{false};
174   bool canceled_ = false; 175   bool canceled_ = false;
175   bool active_ = false; 176   bool active_ = false;
176   177  
177   struct cancel_fn 178   struct cancel_fn
178   { 179   {
179   lock_awaiter* self_; 180   lock_awaiter* self_;
180   181  
HITCBC 181   7 void operator()() const noexcept 182   7 void operator()() const noexcept
182   { 183   {
HITCBC 183   7 if(!self_->claimed_.exchange( 184   7 if(!self_->claimed_.exchange(
184   true, std::memory_order_acq_rel)) 185   true, std::memory_order_acq_rel))
185   { 186   {
HITCBC 186   7 self_->canceled_ = true; 187   7 self_->canceled_ = true;
HITCBC 187   7 self_->ex_.post(self_->cont_); 188   7 self_->ex_.post(self_->cont_);
188   } 189   }
HITCBC 189   7 } 190   7 }
190   }; 191   };
191   192  
192   using stop_cb_t = 193   using stop_cb_t =
193   std::stop_callback<cancel_fn>; 194   std::stop_callback<cancel_fn>;
194   195  
195   // Aligned storage for stop_cb_t. Declared last: 196   // Aligned storage for stop_cb_t. Declared last:
196   // its destructor may block while the callback 197   // its destructor may block while the callback
197   // accesses the members above. 198   // accesses the members above.
198   BOOST_CAPY_MSVC_WARNING_PUSH 199   BOOST_CAPY_MSVC_WARNING_PUSH
199   BOOST_CAPY_MSVC_WARNING_DISABLE(4324) // padded due to alignas 200   BOOST_CAPY_MSVC_WARNING_DISABLE(4324) // padded due to alignas
200   alignas(stop_cb_t) 201   alignas(stop_cb_t)
201   unsigned char stop_cb_buf_[sizeof(stop_cb_t)]; 202   unsigned char stop_cb_buf_[sizeof(stop_cb_t)];
202   BOOST_CAPY_MSVC_WARNING_POP 203   BOOST_CAPY_MSVC_WARNING_POP
203   204  
HITCBC 204   19 stop_cb_t& stop_cb_() noexcept 205   19 stop_cb_t& stop_cb_() noexcept
205   { 206   {
206   return *reinterpret_cast<stop_cb_t*>( 207   return *reinterpret_cast<stop_cb_t*>(
HITCBC 207   19 stop_cb_buf_); 208   19 stop_cb_buf_);
208   } 209   }
209   210  
210   public: 211   public:
  212 + /** Destroy the awaiter, leaving the mutex unable to reach it.
  213 +
  214 + If the awaiter is suspended in the wait queue, destroys the
  215 + stop callback and unlinks the awaiter. Neither `unlock()` nor
  216 + the stop callback can then reach a destroyed awaiter when the
  217 + coroutine frame is torn down while suspended.
  218 +
  219 + @par Preconditions
  220 + Called on the executor thread. The stop callback may fire from
  221 + any thread, so destroying a still-suspended awaiter from
  222 + another thread is undefined.
  223 + */
HITCBC 211   76 ~lock_awaiter() 224   76 ~lock_awaiter()
212   { 225   {
HITCBC 213   76 if(active_) 226   76 if(active_)
214   { 227   {
HITCBC 215   3 stop_cb_().~stop_cb_t(); 228   3 stop_cb_().~stop_cb_t();
HITCBC 216   3 m_->waiters_.remove(this); 229   3 m_->waiters_.remove(this);
217   } 230   }
HITCBC 218   76 } 231   76 }
219   232  
  233 + /** Construct an awaiter for the given mutex.
  234 +
  235 + @param m The mutex to acquire. It must outlive the awaiter.
  236 + */
HITCBC 220   38 explicit lock_awaiter(async_mutex* m) noexcept 237   38 explicit lock_awaiter(async_mutex* m) noexcept
HITCBC 221   38 : m_(m) 238   38 : m_(m)
222   { 239   {
HITCBC 223   38 } 240   38 }
224   241  
  242 + /** Construct by moving.
  243 +
  244 + The moved-from awaiter is left inert: its destructor no longer
  245 + destroys the stop callback and no longer unlinks from the
  246 + mutex's wait queue.
  247 +
  248 + @param o The awaiter to move from.
  249 + */
HITCBC 225   38 lock_awaiter(lock_awaiter&& o) noexcept 250   38 lock_awaiter(lock_awaiter&& o) noexcept
HITCBC 226   76 : m_(o.m_) 251   76 : m_(o.m_)
HITCBC 227   38 , cont_(o.cont_) 252   38 , cont_(o.cont_)
HITCBC 228   38 , ex_(o.ex_) 253   38 , ex_(o.ex_)
HITCBC 229   38 , claimed_(o.claimed_.load( 254   38 , claimed_(o.claimed_.load(
230   std::memory_order_relaxed)) 255   std::memory_order_relaxed))
HITCBC 231   38 , canceled_(o.canceled_) 256   38 , canceled_(o.canceled_)
HITCBC 232   76 , active_(std::exchange(o.active_, false)) 257   76 , active_(std::exchange(o.active_, false))
233   { 258   {
HITCBC 234   38 } 259   38 }
235   260  
236 - lock_awaiter(lock_awaiter const&) = delete; 261 + /** Copy construction is disabled; a waiter is linked into the
237 - lock_awaiter& operator=(lock_awaiter const&) = delete; 262 + mutex's wait queue by address.
238 - lock_awaiter& operator=(lock_awaiter&&) = delete; 263 +
  264 + @param other The awaiter that would be copied.
  265 + */
  266 + lock_awaiter(lock_awaiter const& other) = delete;
  267 +
  268 + /** Copy assignment is disabled; a waiter is linked into the
  269 + mutex's wait queue by address.
  270 +
  271 + @param other The awaiter that would be assigned from.
239   272  
  273 + @return A reference to `*this`.
  274 + */
  275 + lock_awaiter& operator=(lock_awaiter const& other) = delete;
  276 +
  277 + /** Move assignment is disabled; a waiter is linked into the
  278 + mutex's wait queue by address.
  279 +
  280 + @param other The awaiter that would be moved from.
  281 +
  282 + @return A reference to `*this`.
  283 + */
  284 + lock_awaiter& operator=(lock_awaiter&& other) = delete;
  285 +
  286 + /** Acquire the mutex if it is free, reporting whether to suspend.
  287 +
  288 + This is not a pure query: on the fast path it takes the lock.
  289 + When the mutex is unlocked, it marks the mutex locked and
  290 + reports that no suspension is needed. The stop token is not
  291 + consulted, so an uncontended `lock()` succeeds even when stop
  292 + has already been requested.
  293 +
  294 + @return `true` if the mutex was free and is now held by the
  295 + awaiting coroutine. `false` if the mutex is held elsewhere, in
  296 + which case the coroutine suspends.
  297 + */
HITCBC 240   38 bool await_ready() const noexcept 298   38 bool await_ready() const noexcept
241   { 299   {
HITCBC 242   38 if(!m_->locked_) 300   38 if(!m_->locked_)
243   { 301   {
HITCBC 244   17 m_->locked_ = true; 302   17 m_->locked_ = true;
HITCBC 245   17 return true; 303   17 return true;
246   } 304   }
HITCBC 247   21 return false; 305   21 return false;
248   } 306   }
249   307  
250 - /** IoAwaitable protocol overload. */ 308 + /** Enqueue the awaiting coroutine until the mutex is released.
  309 +
  310 + This is the @ref IoAwaitable overload of `await_suspend`.
  311 +
  312 + If a stop request is already pending on `env->stop_token`, the
  313 + awaiter records the cancellation and does not enqueue. The
  314 + mutex is not acquired.
  315 +
  316 + Otherwise it stores `h` and `env->executor`, links itself into
  317 + the back of the mutex's wait queue, and registers a stop
  318 + callback on `env->stop_token`. Whichever of `unlock()` and that
  319 + callback claims the awaiter first posts `h` through the stored
  320 + executor; the other skips it.
  321 +
  322 + @param h The awaiting coroutine, resumed when the mutex is
  323 + acquired or the wait is canceled.
  324 +
  325 + @param env The execution environment. Its executor posts the
  326 + resumption and its stop token is watched for the duration of
  327 + the wait. It must outlive the wait.
  328 +
  329 + @return `h` if a stop request was already pending, which
  330 + resumes the awaiting coroutine immediately without enqueuing
  331 + it. Otherwise `std::noop_coroutine()`, which leaves the
  332 + coroutine suspended and returns control to the resumer.
  333 + */
251   std::coroutine_handle<> 334   std::coroutine_handle<>
HITCBC 252   21 await_suspend( 335   21 await_suspend(
253   std::coroutine_handle<> h, 336   std::coroutine_handle<> h,
254   io_env const* env) noexcept 337   io_env const* env) noexcept
255   { 338   {
HITCBC 256   21 if(env->stop_token.stop_requested()) 339   21 if(env->stop_token.stop_requested())
257   { 340   {
HITCBC 258   2 canceled_ = true; 341   2 canceled_ = true;
HITCBC 259   2 return h; 342   2 return h;
260   } 343   }
HITCBC 261   19 cont_.h = h; 344   19 cont_.h = h;
HITCBC 262   19 ex_ = env->executor; 345   19 ex_ = env->executor;
HITCBC 263   19 m_->waiters_.push_back(this); 346   19 m_->waiters_.push_back(this);
HITCBC 264   57 ::new(stop_cb_buf_) stop_cb_t( 347   57 ::new(stop_cb_buf_) stop_cb_t(
HITCBC 265   19 env->stop_token, cancel_fn{this}); 348   19 env->stop_token, cancel_fn{this});
HITCBC 266   19 active_ = true; 349   19 active_ = true;
HITCBC 267   19 return std::noop_coroutine(); 350   19 return std::noop_coroutine();
268   } 351   }
269   352  
  353 + /** Complete the acquisition and report the outcome.
  354 +
  355 + Destroys the stop callback if one is registered, and unlinks a
  356 + canceled awaiter from the wait queue.
  357 +
  358 + @return An empty `io_result<>` if the mutex is now held by the
  359 + awaiting coroutine. Otherwise one holding `error::canceled`,
  360 + which means the stop token won the race and the mutex is not
  361 + held.
  362 + */
HITCBC 270   35 io_result<> await_resume() noexcept 363   35 io_result<> await_resume() noexcept
271   { 364   {
HITCBC 272   35 if(active_) 365   35 if(active_)
273   { 366   {
HITCBC 274   16 stop_cb_().~stop_cb_t(); 367   16 stop_cb_().~stop_cb_t();
HITCBC 275   16 if(canceled_) 368   16 if(canceled_)
276   { 369   {
HITCBC 277   7 m_->waiters_.remove(this); 370   7 m_->waiters_.remove(this);
HITCBC 278   7 active_ = false; 371   7 active_ = false;
279   return {make_error_code( 372   return {make_error_code(
HITCBC 280   7 error::canceled)}; 373   7 error::canceled)};
281   } 374   }
HITCBC 282   9 active_ = false; 375   9 active_ = false;
283   } 376   }
HITCBC 284   28 if(canceled_) 377   28 if(canceled_)
285   return {make_error_code( 378   return {make_error_code(
HITCBC 286   2 error::canceled)}; 379   2 error::canceled)};
HITCBC 287   26 return {{}}; 380   26 return {{}};
288   } 381   }
289   }; 382   };
290   383  
291 - /** RAII lock guard for async_mutex. 384 + /** Unlocks the mutex automatically when destroyed.
292 -  
293 - Automatically unlocks the mutex when destroyed.  
294   */ 385   */
295   class [[nodiscard]] lock_guard 386   class [[nodiscard]] lock_guard
296   { 387   {
297   async_mutex* m_; 388   async_mutex* m_;
298   389  
299   public: 390   public:
  391 + /// Unlock the mutex, if this guard holds one.
HITCBC 300   9 ~lock_guard() 392   9 ~lock_guard()
301   { 393   {
HITCBC 302   9 if(m_) 394   9 if(m_)
HITCBC 303   2 m_->unlock(); 395   2 m_->unlock();
HITCBC 304   9 } 396   9 }
305   397  
  398 + /// Construct a guard that holds no mutex.
HITCBC 306   2 lock_guard() noexcept 399   2 lock_guard() noexcept
HITCBC 307   2 : m_(nullptr) 400   2 : m_(nullptr)
308   { 401   {
HITCBC 309   2 } 402   2 }
310   403  
  404 + /** Construct a guard that releases the given mutex on destruction.
  405 +
  406 + Adopts an already-held lock; it does not acquire one.
  407 +
  408 + @param m The mutex to unlock on destruction. It must outlive
  409 + the guard.
  410 + */
HITCBC 311   2 explicit lock_guard(async_mutex* m) noexcept 411   2 explicit lock_guard(async_mutex* m) noexcept
HITCBC 312   2 : m_(m) 412   2 : m_(m)
313   { 413   {
HITCBC 314   2 } 414   2 }
315   415  
  416 + /** Construct by moving, transferring the lock.
  417 +
  418 + @par Postconditions
  419 + `o` holds no mutex, and its destructor unlocks nothing.
  420 +
  421 + @param o The guard to move from.
  422 + */
HITCBC 316   5 lock_guard(lock_guard&& o) noexcept 423   5 lock_guard(lock_guard&& o) noexcept
HITCBC 317   5 : m_(std::exchange(o.m_, nullptr)) 424   5 : m_(std::exchange(o.m_, nullptr))
318   { 425   {
HITCBC 319   5 } 426   5 }
320   427  
  428 + /** Assign by moving, transferring the lock.
  429 +
  430 + If this guard already holds a mutex, that mutex is unlocked
  431 + first. Self-assignment is a no-op.
  432 +
  433 + @par Postconditions
  434 + `o` holds no mutex, and its destructor unlocks nothing.
  435 +
  436 + @param o The guard to move from.
  437 +
  438 + @return A reference to `*this`.
  439 + */
321   lock_guard& operator=(lock_guard&& o) noexcept 440   lock_guard& operator=(lock_guard&& o) noexcept
322   { 441   {
323   if(this != &o) 442   if(this != &o)
324   { 443   {
325   if(m_) 444   if(m_)
326   m_->unlock(); 445   m_->unlock();
327   m_ = std::exchange(o.m_, nullptr); 446   m_ = std::exchange(o.m_, nullptr);
328   } 447   }
329   return *this; 448   return *this;
330   } 449   }
331   450  
332 - lock_guard(lock_guard const&) = delete; 451 + /** Copy construction is disabled; a guard uniquely owns the lock.
333 - lock_guard& operator=(lock_guard const&) = delete; 452 +
  453 + @param other The guard that would be copied.
  454 + */
  455 + lock_guard(lock_guard const& other) = delete;
  456 +
  457 + /** Copy assignment is disabled; a guard uniquely owns the lock.
  458 +
  459 + @param other The guard that would be assigned from.
  460 +
  461 + @return A reference to `*this`.
  462 + */
  463 + lock_guard& operator=(lock_guard const& other) = delete;
334   }; 464   };
335   465  
336 - /** Awaiter returned by scoped_lock() that returns a lock_guard on resume. 466 + /** Acquires the mutex like `lock_awaiter`, then resumes with a `lock_guard` that unlocks it.
337   */ 467   */
338   class lock_guard_awaiter 468   class lock_guard_awaiter
339   { 469   {
340   async_mutex* m_; 470   async_mutex* m_;
341   lock_awaiter inner_; 471   lock_awaiter inner_;
342   472  
343   public: 473   public:
  474 + /** Construct an awaiter for the given mutex.
  475 +
  476 + @param m The mutex to acquire. It must outlive the awaiter.
  477 + */
HITCBC 344   4 explicit lock_guard_awaiter(async_mutex* m) noexcept 478   4 explicit lock_guard_awaiter(async_mutex* m) noexcept
HITCBC 345   4 : m_(m) 479   4 : m_(m)
HITCBC 346   4 , inner_(m) 480   4 , inner_(m)
347   { 481   {
HITCBC 348   4 } 482   4 }
349   483  
  484 + /** Acquire the mutex if it is free, reporting whether to suspend.
  485 +
  486 + Delegates to @ref lock_awaiter::await_ready, so as there this is
  487 + not a pure query: on the fast path it takes the lock.
  488 +
  489 + @return `true` if the mutex was free and is now held by the
  490 + awaiting coroutine. `false` if the mutex is held elsewhere, in
  491 + which case the coroutine suspends.
  492 + */
HITCBC 350   4 bool await_ready() const noexcept 493   4 bool await_ready() const noexcept
351   { 494   {
HITCBC 352   4 return inner_.await_ready(); 495   4 return inner_.await_ready();
353   } 496   }
354   497  
355 - /** IoAwaitable protocol overload. */ 498 + /** Enqueue the awaiting coroutine until the mutex is released.
  499 +
  500 + This is the @ref IoAwaitable overload of `await_suspend`. It
  501 + delegates to @ref lock_awaiter::await_suspend on the wrapped
  502 + awaiter, so it has that function's contract.
  503 +
  504 + @param h The awaiting coroutine, resumed when the mutex is
  505 + acquired or the wait is canceled.
  506 +
  507 + @param env The execution environment. Its executor posts the
  508 + resumption and its stop token is watched for the duration of
  509 + the wait. It must outlive the wait.
  510 +
  511 + @return `h` if a stop request was already pending, which
  512 + resumes the awaiting coroutine immediately without enqueuing
  513 + it. Otherwise `std::noop_coroutine()`, which leaves the
  514 + coroutine suspended and returns control to the resumer.
  515 + */
356   std::coroutine_handle<> 516   std::coroutine_handle<>
HITCBC 357   2 await_suspend( 517   2 await_suspend(
358   std::coroutine_handle<> h, 518   std::coroutine_handle<> h,
359   io_env const* env) noexcept 519   io_env const* env) noexcept
360   { 520   {
HITCBC 361   2 return inner_.await_suspend(h, env); 521   2 return inner_.await_suspend(h, env);
362   } 522   }
363   523  
  524 + /** Complete the acquisition and report the outcome.
  525 +
  526 + @return An `io_result<lock_guard>` destructuring as
  527 + `[ec, guard]`. On success `ec` is empty and `guard` holds the
  528 + mutex, releasing it when destroyed. If the wait was canceled,
  529 + `ec` is `error::canceled` and `guard` holds no mutex.
  530 + */
HITCBC 364   4 io_result<lock_guard> await_resume() noexcept 531   4 io_result<lock_guard> await_resume() noexcept
365   { 532   {
HITCBC 366   4 auto r = inner_.await_resume(); 533   4 auto r = inner_.await_resume();
HITCBC 367   4 if(r.ec) 534   4 if(r.ec)
HITCBC 368   2 return {r.ec, {}}; 535   2 return {r.ec, {}};
HITCBC 369   2 return {{}, lock_guard(m_)}; 536   2 return {{}, lock_guard(m_)};
370   } 537   }
371   }; 538   };
372   539  
373   /// Construct an unlocked mutex. 540   /// Construct an unlocked mutex.
374   async_mutex() = default; 541   async_mutex() = default;
375   542  
376 - /// Copy constructor (deleted). 543 + /** Copy construction is disabled; suspended waiters point into the
377 - async_mutex(async_mutex const&) = delete; 544 + mutex's wait queue.
378   545  
379 - /// Copy assignment (deleted). 546 + @param other The mutex that would be copied.
380 - async_mutex& operator=(async_mutex const&) = delete; 547 + */
  548 + async_mutex(async_mutex const& other) = delete;
381   549  
382 - /// Move constructor (deleted). 550 + /** Copy assignment is disabled; suspended waiters point into the
383 - async_mutex(async_mutex&&) = delete; 551 + mutex's wait queue.
384   552  
385 - /// Move assignment (deleted). 553 + @param other The mutex that would be assigned from.
386 - async_mutex& operator=(async_mutex&&) = delete; 554 +
  555 + @return A reference to `*this`.
  556 + */
  557 + async_mutex& operator=(async_mutex const& other) = delete;
  558 +
  559 + /** Move construction is disabled; suspended waiters point into the
  560 + mutex's wait queue.
  561 +
  562 + @param other The mutex that would be moved from.
  563 + */
  564 + async_mutex(async_mutex&& other) = delete;
  565 +
  566 + /** Move assignment is disabled; suspended waiters point into the
  567 + mutex's wait queue.
  568 +
  569 + @param other The mutex that would be moved from.
  570 +
  571 + @return A reference to `*this`.
  572 + */
  573 + async_mutex& operator=(async_mutex&& other) = delete;
387   574  
388   /** Returns an awaiter that acquires the mutex. 575   /** Returns an awaiter that acquires the mutex.
389   576  
390   @return An awaitable that await-returns `(error_code)`. 577   @return An awaitable that await-returns `(error_code)`.
391   */ 578   */
HITCBC 392   34 lock_awaiter lock() noexcept 579   34 lock_awaiter lock() noexcept
393   { 580   {
HITCBC 394   34 return lock_awaiter{this}; 581   34 return lock_awaiter{this};
395   } 582   }
396   583  
397   /** Returns an awaiter that acquires the mutex with RAII. 584   /** Returns an awaiter that acquires the mutex with RAII.
398   585  
399   @return An awaitable that await-returns `(error_code,lock_guard)`. 586   @return An awaitable that await-returns `(error_code,lock_guard)`.
400   */ 587   */
HITCBC 401   4 lock_guard_awaiter scoped_lock() noexcept 588   4 lock_guard_awaiter scoped_lock() noexcept
402   { 589   {
HITCBC 403   4 return lock_guard_awaiter(this); 590   4 return lock_guard_awaiter(this);
404   } 591   }
405   592  
406   /** Releases the mutex. 593   /** Releases the mutex.
407   594  
408   If waiters are queued, the next eligible waiter is 595   If waiters are queued, the next eligible waiter is
409   resumed with the lock held. Canceled waiters are 596   resumed with the lock held. Canceled waiters are
410   skipped. If no eligible waiter remains, the mutex 597   skipped. If no eligible waiter remains, the mutex
411   becomes unlocked. 598   becomes unlocked.
412   */ 599   */
HITCBC 413   26 void unlock() noexcept 600   26 void unlock() noexcept
414   { 601   {
415   for(;;) 602   for(;;)
416   { 603   {
HITCBC 417   27 auto* waiter = waiters_.pop_front(); 604   27 auto* waiter = waiters_.pop_front();
HITCBC 418   27 if(!waiter) 605   27 if(!waiter)
419   { 606   {
HITCBC 420   17 locked_ = false; 607   17 locked_ = false;
HITCBC 421   17 return; 608   17 return;
422   } 609   }
HITCBC 423   10 if(!waiter->claimed_.exchange( 610   10 if(!waiter->claimed_.exchange(
424   true, std::memory_order_acq_rel)) 611   true, std::memory_order_acq_rel))
425   { 612   {
HITCBC 426   9 waiter->ex_.post(waiter->cont_); 613   9 waiter->ex_.post(waiter->cont_);
HITCBC 427   9 return; 614   9 return;
428   } 615   }
HITCBC 429   1 } 616   1 }
430   } 617   }
431   618  
432   /** Returns true if the mutex is currently locked. 619   /** Returns true if the mutex is currently locked.
  620 +
  621 + @return `true` if the mutex is held; otherwise `false`.
433   */ 622   */
HITCBC 434   27 bool is_locked() const noexcept 623   27 bool is_locked() const noexcept
435   { 624   {
HITCBC 436   27 return locked_; 625   27 return locked_;
437   } 626   }
438   }; 627   };
439   628  
440   } // namespace capy 629   } // namespace capy
441   } // namespace boost 630   } // namespace boost
442   631  
443   #endif 632   #endif