...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
In contrast to asymmetric coroutines, where the relationship between caller and callee is fixed, symmetric coroutines are able to transfer execution control to any other (symmetric) coroutine. E.g. a symmetric coroutine is not required to return to its direct caller.
symmetric_coroutine<>::call_type starts a symmetric coroutine and transfers its parameter to its coroutine-function. The template parameter defines the transferred parameter type. The constructor of symmetric_coroutine<>::call_type takes a function (coroutine-function) accepting a reference to a symmetric_coroutine<>::yield_type as argument. Instantiating a symmetric_coroutine<>::call_type does not pass the control of execution to coroutine-function - instead the first call of symmetric_coroutine<>::call_type::operator() synthesizes a symmetric_coroutine<>::yield_type and passes it as reference to coroutine-function.
The symmetric_coroutine<>::call_type interface does not contain a get()-function: you can not retrieve values from another execution context with this kind of coroutine object.
symmetric_coroutine<>::yield_type::operator() is used to transfer data and execution control to another context by calling symmetric_coroutine<>::yield_type::operator() with another symmetric_coroutine<>::call_type as first argument. Alternatively, you may transfer control back to the code that called symmetric_coroutine<>::call_type::operator() by calling symmetric_coroutine<>::yield_type::operator() without a symmetric_coroutine<>::call_type argument.
The class has only one template parameter defining the transferred parameter type. Data transferred to the coroutine are accessed through symmetric_coroutine<>::yield_type::get().
Important | |
---|---|
symmetric_coroutine<>::yield_type can only be created by the framework. |
std::vector<int> merge(const std::vector<int>& a,const std::vector<int>& b) { std::vector<int> c; std::size_t idx_a=0,idx_b=0; boost::coroutines::symmetric_coroutine<void>::call_type* other_a=0,* other_b=0; boost::coroutines::symmetric_coroutine<void>::call_type coro_a( [&](boost::coroutines::symmetric_coroutine<void>::yield_type& yield) { while(idx_a<a.size()) { if(b[idx_b]<a[idx_a]) // test if element in array b is less than in array a yield(*other_b); // yield to coroutine coro_b c.push_back(a[idx_a++]); // add element to final array } // add remaining elements of array b while ( idx_b < b.size()) c.push_back( b[idx_b++]); }); boost::coroutines::symmetric_coroutine<void>::call_type coro_b( [&](boost::coroutines::symmetric_coroutine<void>::yield_type& yield) { while(idx_b<b.size()) { if (a[idx_a]<b[idx_b]) // test if element in array a is less than in array b yield(*other_a); // yield to coroutine coro_a c.push_back(b[idx_b++]); // add element to final array } // add remaining elements of array a while ( idx_a < a.size()) c.push_back( a[idx_a++]); }); other_a = & coro_a; other_b = & coro_b; coro_a(); // enter coroutine-fn of coro_a return c; } std::vector< int > a = {1,5,6,10}; std::vector< int > b = {2,4,7,8,9,13}; std::vector< int > c = merge(a,b); print(a); print(b); print(c); output: a : 1 5 6 10 b : 2 4 7 8 9 13 c : 1 2 4 5 6 7 8 9 10 13
In this example two symmetric_coroutine<>::call_type
are created in the main execution context accepting a lambda function (==
coroutine-function) which merges elements of two sorted
arrays into a third array. coro_a()
enters the coroutine-function
of coro_a
cycling through
the array and testing if the actual element in the other array is less than
the element in the local one. If so, the coroutine yields to the other coroutine
coro_b
using yield(*other_b)
.
If the current element of the local array is less than the element of the
other array, it is put to the third array. Because the coroutine jumps back
to coro_a()
(returning from this method) after leaving the coroutine-function,
the elements of the other array will appended at the end of the third array
if all element of the local array are processed.
The coroutine-function returns void and takes symmetric_coroutine<>::yield_type, providing coroutine functionality inside the coroutine-function, as argument. Using this instance is the only way to transfer data and execution control. symmetric_coroutine<>::call_type does not enter the coroutine-function at symmetric_coroutine<>::call_type construction but at the first invocation of symmetric_coroutine<>::call_type::operator().
Unless the template parameter is void
,
the coroutine-function of a symmetric_coroutine<>::call_type
can assume that (a) upon initial entry and (b) after every symmetric_coroutine<>::yield_type::operator()
call, its symmetric_coroutine<>::yield_type::get()
has a new value available.
However, if the template parameter is a move-only type, symmetric_coroutine<>::yield_type::get() may only be called once before the next symmetric_coroutine<>::yield_type::operator() call.
In order to transfer data to a symmetric_coroutine<>::call_type from the main-context the framework synthesizes a symmetric_coroutine<>::yield_type associated with the symmetric_coroutine<>::call_type instance. The synthesized symmetric_coroutine<>::yield_type is passed as argument to coroutine-function. The main-context must call symmetric_coroutine<>::call_type::operator() in order to transfer each data value into the coroutine-function. Access to the transferred data value is given by symmetric_coroutine<>::yield_type::get().
boost::coroutines::symmetric_coroutine<int>::call_type coro( // constructor does NOT enter coroutine-function [&](boost::coroutines::symmetric_coroutine<int>::yield_type& yield){ for (;;) { std::cout << yield.get() << " "; yield(); // jump back to starting context } }); coro(1); // transfer {1} to coroutine-function coro(2); // transfer {2} to coroutine-function coro(3); // transfer {3} to coroutine-function coro(4); // transfer {4} to coroutine-function coro(5); // transfer {5} to coroutine-function
An uncaught exception inside a symmetric_coroutine<>::call_type's coroutine-function will call std::terminate().
Important | |
---|---|
Code executed by coroutine must not prevent the propagation of the detail::forced_unwind exception. Absorbing that exception will cause stack unwinding to fail. Thus, any code that catches all exceptions must re-throw any pending detail::forced_unwind exception. |
try { // code that might throw } catch(const boost::coroutines::detail::forced_unwind&) { throw; } catch(...) { // possibly not re-throw pending exception }
Important | |
---|---|
Do not jump from inside a catch block and then re-throw the exception in another execution context. |
Sometimes it is necessary to unwind the stack of an unfinished coroutine
to destroy local stack variables so they can release allocated resources
(RAII pattern). The attributes
argument of the coroutine constructor indicates whether the destructor should
unwind the stack (stack is unwound by default).
Stack unwinding assumes the following preconditions:
After unwinding, a coroutine is complete.
struct X { X(){ std::cout<<"X()"<<std::endl; } ~X(){ std::cout<<"~X()"<<std::endl; } }; boost::coroutines::symmetric_coroutine<int>::call_type other_coro(...); { boost::coroutines::symmetric_coroutine<void>::call_type coro( [&](boost::coroutines::symmetric_coroutine<void>::yield_type& yield){ X x; std::cout<<"fn()"<<std::endl; // transfer execution control to other coroutine yield( other_coro, 7); }); coro(); std::cout<<"coro is complete: "<<std::boolalpha<<!coro<<"\n"; } output: X() fn() coro is complete: false ~X()
coroutine-function is exited with a simple return statement.
This jumps back to the calling symmetric_coroutine<>::call_type::operator()
at the start of symmetric coroutine chain. That is, symmetric coroutines
do not have a strong, fixed relationship to the caller as do asymmetric coroutines.
The symmetric_coroutine<>::call_type becomes complete,
e.g. symmetric_coroutine<>::call_type::operator bool
will return false
.
Important | |
---|---|
After returning from coroutine-function the coroutine is complete (can not be resumed with symmetric_coroutine<>::call_type::operator()). |