Strands
This page explains how to use strands to serialize coroutine execution.
Code snippets assume using namespace boost::capy; is in effect.
|
The Problem
When multiple coroutines access shared state concurrently, you need synchronization. Traditional mutexes block threads, wasting resources. Strands provide an alternative: guarantee that only one coroutine runs at a time without blocking.
// Without synchronization: data race
int counter = 0;
task<void> increment()
{
++counter; // UNSAFE: concurrent access
co_return;
}
// Running on a thread pool with 4 threads
run_async(pool.get_executor())(when_all(
increment(),
increment(),
increment()
));
What is a Strand?
A strand wraps an executor and ensures that coroutines dispatched through it never run concurrently. At most one coroutine executes within the strand at any given time:
#include <boost/capy/ex/strand.hpp>
thread_pool pool(4);
strand s(pool.get_executor());
// These coroutines will never run concurrently
s.post(coro1);
s.post(coro2);
s.post(coro3);
// They may run on different threads, but one at a time
Creating a Strand
Construct a strand by wrapping an existing executor:
thread_pool pool(4);
// Create a strand from the pool's executor
strand s(pool.get_executor());
// Or use the deduction guide
strand s2{pool.get_executor()};
The strand type is templated on the inner executor:
strand<thread_pool::executor_type> s(pool.get_executor());
Strand as Executor
Strands satisfy the Executor concept and can be used anywhere an executor
is expected:
strand s(pool.get_executor());
// Use strand with run_async
run_async(s)(my_task());
// Use strand with run_on
co_await run_on(s, other_task());
Post vs Dispatch
Strands provide two methods for submitting coroutines:
| Method | Behavior |
|---|---|
|
Always queues the coroutine, guaranteeing FIFO ordering |
|
If already in the strand, resumes immediately; otherwise queues |
FIFO Ordering with post
Use post when ordering matters:
s.post(first);
s.post(second);
s.post(third);
// Execution order: first, second, third (guaranteed)
Inline Execution with dispatch
Use dispatch for performance when ordering doesn’t matter:
// If we're already in the strand, dispatch resumes inline
s.dispatch(continuation); // May run immediately
Dispatch provides symmetric transfer when the caller is already in the strand’s execution context, avoiding unnecessary queuing.
Protecting Shared State
Use a strand to serialize access to shared data:
class counter
{
strand<thread_pool::executor_type> strand_;
int value_ = 0;
public:
explicit counter(thread_pool& pool)
: strand_(pool.get_executor())
{
}
// Increment must run on the strand
task<void> increment()
{
co_await run_on(strand_, [this]() -> task<void> {
++value_; // Safe: only one coroutine at a time
co_return;
}());
}
// Read also runs on the strand
task<int> get()
{
co_return co_await run_on(strand_, [this]() -> task<int> {
co_return value_;
}());
}
};
Strand Identity
Strands are lightweight handles. Copies share the same serialization state:
strand s1(pool.get_executor());
strand s2 = s1; // Same strand, same serialization
s1.post(coro1);
s2.post(coro2);
// coro1 and coro2 are serialized with respect to each other
Compare strands to check if they serialize:
if (s1 == s2)
{
// Same strand — coroutines will be serialized
}
running_in_this_thread
Check if the current thread is executing within a strand:
strand s(pool.get_executor());
void callback()
{
if (s.running_in_this_thread())
{
// We're in the strand — safe to access protected data
}
else
{
// Not in strand — need to post/dispatch
}
}
Implementation Notes
Capy’s strand uses a fixed pool of 211 implementation objects. New strands hash to select an impl from the pool. Strands that hash to the same index share serialization:
-
This is harmless — just extra serialization
-
Rare with 211 buckets
-
No allocation for strand creation
This design trades minimal extra serialization for zero per-strand allocation.
When NOT to Use Strands
Use strands when:
-
Coroutines share mutable state
-
You want to avoid blocking threads
-
Operations are I/O-bound (not CPU-intensive)
Do NOT use strands when:
-
You need fine-grained locking (use
async_mutexinstead) -
Operations are CPU-intensive — one long operation blocks others
-
You need cross-context synchronization — strands are per-executor
Summary
| Feature | Description |
|---|---|
|
Wraps executor |
|
Always queue (strict FIFO) |
|
Inline if in strand, else queue |
|
Check if caller is in the strand |
Copies |
Share serialization state |
Next Steps
-
Executor Affinity — How
run_onchanges executors -
Thread Pool — The underlying executor