Async Mutex

Fair FIFO coroutine locking with async_mutex.

What You Will Learn

  • Using async_mutex for mutual exclusion between coroutines

  • RAII lock guards with scoped_lock

  • FIFO fairness guarantees

  • Comparing async_mutex to strand-based serialization

Prerequisites

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.

FIFO Fairness

Workers acquire the lock in the order they request it. Unlike std::mutex, which has no fairness guarantees, async_mutex ensures FIFO ordering — the first coroutine to call scoped_lock() is the first to acquire it.

Strand vs Async Mutex

The strand serialization example showed how a strand can protect shared state by running all coroutines sequentially. async_mutex provides finer-grained control: coroutines run concurrently and only serialize when entering the critical section.

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

  1. Add work outside the critical section (before and after scoped_lock) to observe concurrent execution

  2. Use a stop token to cancel waiting workers after a timeout

  3. Replace async_mutex with a strand and compare the two approaches

Next Steps