Strand Serialization
Protecting shared state with a strand instead of a mutex.
What You Will Learn
-
Using a
strandto serialize coroutine access to shared state -
Lock-free shared state management
-
Combining
when_allwith strand-based serialization
Prerequisites
-
Completed Producer-Consumer (introduces
strand) -
Understanding of
when_allfrom Composition
Source Code
#include <boost/capy.hpp>
#include <iostream>
#include <latch>
using namespace boost::capy;
int main()
{
constexpr int num_coroutines = 10;
constexpr int increments_per_coro = 1000;
thread_pool pool(4);
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();
};
int counter = 0;
// Each coroutine increments the shared counter without locks.
// The strand ensures only one coroutine runs at a time.
auto increment = [&](int id) -> task<> {
for (int i = 0; i < increments_per_coro; ++i)
++counter;
std::cout << "Coroutine " << id
<< " finished, counter = " << counter << "\n";
co_return;
};
auto run_all = [&]() -> task<> {
co_await when_all(
increment(0), increment(1), increment(2),
increment(3), increment(4), increment(5),
increment(6), increment(7), increment(8),
increment(9));
};
run_async(s, on_complete, on_error)(run_all());
done.wait();
int expected = num_coroutines * increments_per_coro;
std::cout << "\nFinal counter: " << counter
<< " (expected " << expected << ")\n";
return 0;
}
Build
add_executable(strand_serialization strand_serialization.cpp)
target_link_libraries(strand_serialization PRIVATE Boost::capy)
Walkthrough
Strand as Serializer
strand s{pool.get_executor()};
A strand wraps an executor and guarantees that handlers dispatched through it never run concurrently. This replaces the need for a mutex when protecting shared state accessed by coroutines.
Lock-Free Shared Access
int counter = 0;
auto increment = [&](int id) -> task<> {
for (int i = 0; i < increments_per_coro; ++i)
++counter;
// ...
};
Multiple coroutines increment the same counter without any locks. The strand serializes execution so only one coroutine runs at a time, preventing data races.
Output
Coroutine 0 finished, counter = 1000
Coroutine 1 finished, counter = 2000
Coroutine 2 finished, counter = 3000
Coroutine 3 finished, counter = 4000
Coroutine 4 finished, counter = 5000
Coroutine 5 finished, counter = 6000
Coroutine 6 finished, counter = 7000
Coroutine 7 finished, counter = 8000
Coroutine 8 finished, counter = 9000
Coroutine 9 finished, counter = 10000
Final counter: 10000 (expected 10000)
Exercises
-
Remove the strand and run directly on the pool executor — observe the data race
-
Replace the plain
intcounter withstd::atomic<int>and compare the two approaches -
Add a second shared variable and verify both are protected by the strand
Next Steps
-
Async Mutex — FIFO coroutine locking