Synchronization Primitives
This section explains coroutine-friendly synchronization primitives.
Code examples assume using namespace boost::capy; is in effect.
|
Why Coroutine-Specific Primitives?
Standard mutexes block the entire thread:
std::mutex mtx;
task<void> bad_example()
{
std::lock_guard lock(mtx); // Blocks thread!
co_await some_operation(); // Still holding lock
} // Other coroutines on this thread can't run
Coroutine-aware primitives suspend the coroutine instead:
async_mutex mtx;
task<void> good_example()
{
auto lock = co_await mtx.lock(); // Suspends coroutine, not thread
co_await some_operation();
} // Other coroutines can run while we wait
async_mutex
A mutex that suspends waiting coroutines instead of blocking threads:
#include <boost/capy/ex/async_mutex.hpp>
async_mutex mtx;
task<void> critical_section()
{
auto lock = co_await mtx.lock();
// Exclusive access to protected resource
modify_shared_data();
} // Lock released when lock goes out of scope
Usage
Acquire lock:
auto lock = co_await mtx.lock();
RAII release:
{
auto lock = co_await mtx.lock();
// ... protected region ...
} // Lock released here
Manual release:
auto lock = co_await mtx.lock();
// ...
lock.unlock(); // Release early
async_event
A single-shot event for coroutine synchronization:
#include <boost/capy/ex/async_event.hpp>
async_event event;
task<void> waiter()
{
co_await event.wait(); // Suspends until signaled
// Event was signaled
}
task<void> signaler()
{
// Do some work...
event.set(); // Wake all waiters
}
Usage
Wait for event:
co_await event.wait();
Signal event:
event.set(); // Wake all waiting coroutines
Reset for reuse:
event.reset(); // Clear signaled state
One-Shot vs Reusable
async_event can be reset for reuse, but care is needed:
// Safe pattern: signal once
event.set(); // All waiters wake
// Reuse pattern: reset before new waiters
event.reset();
// Now coroutines can wait again
co_await event.wait();
| Don’t reset while coroutines are actively waiting—they may miss the signal. |
Patterns
Producer-Consumer
async_mutex mtx;
std::queue<item> queue;
async_event not_empty;
task<void> producer()
{
while (running)
{
auto item = co_await produce();
{
auto lock = co_await mtx.lock();
queue.push(item);
}
not_empty.set();
}
}
task<void> consumer()
{
while (running)
{
co_await not_empty.wait();
item i;
{
auto lock = co_await mtx.lock();
if (!queue.empty())
{
i = queue.front();
queue.pop();
if (queue.empty())
not_empty.reset();
}
}
co_await consume(i);
}
}
When NOT to Use
Prefer strand when:
-
You need serialized access to an object
-
Operations naturally form a sequence
-
You want to avoid explicit locking
Prefer standard primitives when:
-
Blocking is acceptable
-
No coroutines are involved
-
Performance is critical (no await overhead)
Summary
| Primitive | Purpose |
|---|---|
|
Mutual exclusion without blocking threads |
|
Signal completion to waiting coroutines |
|
Acquire mutex, suspending if held |
|
Attempt to acquire without waiting |
|
Signal event, wake all waiters |
|
Suspend until event is signaled |
Next Steps
-
Executors and Strands — Alternative serialization
-
Concurrent Composition — Coordinate parallel tasks