Async Mutex
Fair FIFO coroutine locking with async_mutex.
What You Will Learn
-
Using
async_mutexfor mutual exclusion between coroutines -
RAII lock guards with
scoped_lock -
FIFO fairness guarantees
-
Comparing
async_mutexto strand-based serialization
Prerequisites
-
Completed Strand Serialization
Source Code
#include <boost/capy.hpp>
#include <iostream>
#include <latch>
#include <vector>
using namespace boost::capy;
int main()
{
constexpr int num_workers = 6;
thread_pool pool;
strand s{pool.get_executor()};
std::latch done(1);
auto on_complete = [&done](auto&&...) { done.count_down(); };
auto on_error = [&done](std::exception_ptr ep) {
try { std::rethrow_exception(ep); }
catch (std::exception const& e) {
std::cerr << "Error: " << e.what() << "\n";
}
catch (...) {
std::cerr << "Error: unknown exception\n";
}
done.count_down();
};
async_mutex mtx;
int acquisition_order = 0;
std::vector<int> order_log;
auto worker = [&](int id) -> task<> {
std::cout << "Worker " << id << " waiting for lock\n";
auto [ec, guard] = co_await mtx.scoped_lock();
if (ec)
{
std::cout << "Worker " << id
<< " canceled: " << ec.message() << "\n";
co_return;
}
int seq = acquisition_order++;
order_log.push_back(id);
std::cout << "Worker " << id
<< " acquired lock (sequence " << seq << ")\n";
// Simulate work inside the critical section
std::cout << "Worker " << id << " releasing lock\n";
co_return;
};
auto run_all = [&]() -> task<> {
co_await when_all(
worker(0), worker(1), worker(2),
worker(3), worker(4), worker(5));
};
// Run on a strand so async_mutex operations are single-threaded
run_async(s, on_complete, on_error)(run_all());
done.wait();
std::cout << "\nAcquisition order: ";
for (std::size_t i = 0; i < order_log.size(); ++i)
{
if (i > 0)
std::cout << " -> ";
std::cout << "W" << order_log[i];
}
std::cout << "\n";
return 0;
}
Build
add_executable(async_mutex async_mutex.cpp)
target_link_libraries(async_mutex PRIVATE Boost::capy)
Walkthrough
Creating the Mutex
async_mutex mtx;
async_mutex is a coroutine-aware mutex. Unlike std::mutex, it suspends the calling coroutine instead of blocking the thread, allowing other coroutines to run while waiting for the lock.
Scoped Lock
auto [ec, guard] = co_await mtx.scoped_lock();
if (ec)
{
// Lock was canceled
co_return;
}
scoped_lock() returns an io_result with an error code and an RAII guard. The guard automatically releases the lock when it goes out of scope. If the operation is canceled (e.g., via a stop token), ec will be set.
Output
Worker 0 waiting for lock
Worker 0 acquired lock (sequence 0)
Worker 0 releasing lock
Worker 1 waiting for lock
Worker 1 acquired lock (sequence 1)
Worker 1 releasing lock
Worker 2 waiting for lock
Worker 2 acquired lock (sequence 2)
Worker 2 releasing lock
Worker 3 waiting for lock
Worker 3 acquired lock (sequence 3)
Worker 3 releasing lock
Worker 4 waiting for lock
Worker 4 acquired lock (sequence 4)
Worker 4 releasing lock
Worker 5 waiting for lock
Worker 5 acquired lock (sequence 5)
Worker 5 releasing lock
Acquisition order: W0 -> W1 -> W2 -> W3 -> W4 -> W5
Exercises
-
Add work outside the critical section (before and after
scoped_lock) to observe concurrent execution -
Use a stop token to cancel waiting workers after a timeout
-
Replace
async_mutexwith a strand and compare the two approaches
Next Steps
-
Parallel Tasks — Distributing work across a thread pool