Symmetric coroutine

| Home | Libraries | People | FAQ | More

### Symmetric coroutine

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

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

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.

##### coroutine-function

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.

##### passing data from main-context to a symmetric-coroutine

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

##### exceptions

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.

##### Stack unwinding

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:

- The coroutine is not not-a-coroutine
- The coroutine is not complete
- The coroutine is not running
- The coroutine owns a stack

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()

##### Exit a coroutine-function

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()).