Asymmetric coroutine

| Home | Libraries | People | FAQ | More

### Asymmetric coroutine

Two asymmetric coroutine types - asymmetric_coroutine<>::push_type and asymmetric_coroutine<>::pull_type - provide a unidirectional transfer of data.

##### asymmetric_coroutine<>::pull_type

asymmetric_coroutine<>::pull_type transfers data from another execution context (== pulled-from). The template parameter defines the transferred parameter type. The constructor of asymmetric_coroutine<>::pull_type takes a function (coroutine-function) accepting a reference to an asymmetric_coroutine<>::push_type as argument. Instantiating an asymmetric_coroutine<>::pull_type passes the control of execution to coroutine-function and a complementary asymmetric_coroutine<>::push_type is synthesized by the library and passed as reference to coroutine-function.

This kind of coroutine provides asymmetric_coroutine<>::pull_type::operator(). This method only switches context; it transfers no data.

asymmetric_coroutine<>::pull_type provides input iterators (asymmetric_coroutine<>::pull_type::iterator) and std::begin()/std::end() are overloaded. The increment-operation switches the context and transfers data.

    boost::coroutines::asymmetric_coroutine<int>::pull_type source(
        [&](boost::coroutines::asymmetric_coroutine<int>::push_type& sink){
            int first=1,second=1;
            sink(first);
            sink(second);
            for(int i=0;i<8;++i){
                int third=first+second;
                first=second;
                second=third;
                sink(third);
            }
        });

    for(auto i:source)
        std::cout << i <<  " ";

    output:
    1 1 2 3 5 8 13 21 34 55

In this example an asymmetric_coroutine<>::pull_type is created in the main execution context taking a lambda function (== coroutine-function) which calculates Fibonacci numbers in a simple for-loop. The coroutine-function is executed in a newly created execution context which is managed by the instance of asymmetric_coroutine<>::pull_type. An asymmetric_coroutine<>::push_type is automatically generated by the library and passed as reference to the lambda function. Each time the lambda function calls asymmetric_coroutine<>::push_type::operator() with another Fibonacci number, asymmetric_coroutine<>::push_type transfers it back to the main execution context. The local state of coroutine-function is preserved and will be restored upon transferring execution control back to coroutine-function to calculate the next Fibonacci number. Because asymmetric_coroutine<>::pull_type provides input iterators and std::begin()/std::end() are overloaded, a range-based for-loop can be used to iterate over the generated Fibonacci numbers.

##### asymmetric_coroutine<>::push_type

asymmetric_coroutine<>::push_type transfers data to the other execution context (== pushed-to). The template parameter defines the transferred parameter type. The constructor of asymmetric_coroutine<>::push_type takes a function (coroutine-function) accepting a reference to an asymmetric_coroutine<>::pull_type as argument. In contrast to asymmetric_coroutine<>::pull_type, instantiating an asymmetric_coroutine<>::push_type does not pass the control of execution to coroutine-function - instead the first call of asymmetric_coroutine<>::push_type::operator() synthesizes a complementary asymmetric_coroutine<>::pull_type and passes it as reference to coroutine-function.

The asymmetric_coroutine<>::push_type interface does not contain a get()-function: you can not retrieve values from another execution context with this kind of coroutine.

asymmetric_coroutine<>::push_type provides output iterators (asymmetric_coroutine<>::push_type::iterator) and std::begin()/std::end() are overloaded. The increment-operation switches the context and transfers data.

    struct FinalEOL{
        ~FinalEOL(){
            std::cout << std::endl;
        }
    };

    const int num=5, width=15;
    boost::coroutines::asymmetric_coroutine<std::string>::push_type writer(
        [&](boost::coroutines::asymmetric_coroutine<std::string>::pull_type& in){
            // finish the last line when we leave by whatever means
            FinalEOL eol;
            // pull values from upstream, lay them out 'num' to a line
            for (;;){
                for(int i=0;i<num;++i){
                    // when we exhaust the input, stop
                    if(!in) return;
                    std::cout << std::setw(width) << in.get();
                    // now that we've handled this item, advance to next
                    in();
                }
                // after 'num' items, line break
                std::cout << std::endl;
            }
        });

    std::vector<std::string> words{
        "peas", "porridge", "hot", "peas",
        "porridge", "cold", "peas", "porridge",
        "in", "the", "pot", "nine",
        "days", "old" };

    std::copy(boost::begin(words),boost::end(words),boost::begin(writer));

    output:
               peas       porridge            hot           peas       porridge
               cold           peas       porridge             in            the
                pot           nine           days            old

In this example an asymmetric_coroutine<>::push_type is created in the main execution context accepting a lambda function (== coroutine-function) which requests strings and lays out 'num' of them on each line. This demonstrates the inversion of control permitted by coroutines. Without coroutines, a utility function to perform the same job would necessarily accept each new value as a function parameter, returning after processing that single value. That function would depend on a static state variable. A coroutine-function, however, can request each new value as if by calling a function -- even though its caller also passes values as if by calling a function. The coroutine-function is executed in a newly created execution context which is managed by the instance of asymmetric_coroutine<>::push_type. The main execution context passes the strings to the coroutine-function by calling asymmetric_coroutine<>::push_type::operator(). An asymmetric_coroutine<>::pull_type instance is automatically generated by the library and passed as reference to the lambda function. The coroutine-function accesses the strings passed from the main execution context by calling asymmetric_coroutine<>::pull_type::get() and lays those strings out on std::cout according the parameters 'num' and 'width'. The local state of coroutine-function is preserved and will be restored after transferring execution control back to coroutine-function. Because asymmetric_coroutine<>::push_type provides output iterators and std::begin()/std::end() are overloaded, the std::copy algorithm can be used to iterate over the vector containing the strings and pass them one by one to the coroutine.

##### coroutine-function

The coroutine-function returns void and takes its counterpart-coroutine as argument, so that using the coroutine passed as argument to coroutine-function is the only way to transfer data and execution control back to the caller. Both coroutine types take the same template argument. For asymmetric_coroutine<>::pull_type the coroutine-function is entered at asymmetric_coroutine<>::pull_type construction. For asymmetric_coroutine<>::push_type the coroutine-function is not entered at asymmetric_coroutine<>::push_type construction but entered by the first invocation of asymmetric_coroutine<>::push_type::operator(). After execution control is returned from coroutine-function the state of the coroutine can be checked via asymmetric_coroutine<>::pull_type::operator bool returning `true` if the coroutine is still valid (coroutine-function has not terminated). Unless the first template parameter is `void`, `true` also implies that a data value is available.

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

In order to transfer data from an asymmetric_coroutine<>::pull_type to the main-context the framework synthesizes an asymmetric_coroutine<>::push_type associated with the asymmetric_coroutine<>::pull_type instance in the main-context. The synthesized asymmetric_coroutine<>::push_type is passed as argument to coroutine-function. The coroutine-function must call this asymmetric_coroutine<>::push_type::operator() in order to transfer each data value back to the main-context. In the main-context, the asymmetric_coroutine<>::pull_type::operator bool determines whether the coroutine is still valid and a data value is available or coroutine-function has terminated (asymmetric_coroutine<>::pull_type is invalid; no data value available). Access to the transferred data value is given by asymmetric_coroutine<>::pull_type::get().

    boost::coroutines::asymmetric_coroutine<int>::pull_type source( // constructor enters coroutine-function
        [&](boost::coroutines::asymmetric_coroutine<int>::push_type& sink){
            sink(1); // push {1} back to main-context
            sink(1); // push {1} back to main-context
            sink(2); // push {2} back to main-context
            sink(3); // push {3} back to main-context
            sink(5); // push {5} back to main-context
            sink(8); // push {8} back to main-context
        });

    while(source){            // test if pull-coroutine is valid
        int ret=source.get(); // access data value
        source();             // context-switch to coroutine-function
    }

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

In order to transfer data to an asymmetric_coroutine<>::push_type from the main-context the framework synthesizes an asymmetric_coroutine<>::pull_type associated with the asymmetric_coroutine<>::push_type instance in the main-context. The synthesized asymmetric_coroutine<>::pull_type is passed as argument to coroutine-function. The main-context must call this asymmetric_coroutine<>::push_type::operator() in order to transfer each data value into the coroutine-function. Access to the transferred data value is given by asymmetric_coroutine<>::pull_type::get().

    boost::coroutines::asymmetric_coroutine<int>::push_type sink( // constructor does NOT enter coroutine-function
        [&](boost::coroutines::asymmetric_coroutine<int>::pull_type& source){
            for (int i:source) {
                std::cout << i <<  " ";
            }
        });

    std::vector<int> v{1,1,2,3,5,8,13,21,34,55};
    for( int i:v){
        sink(i); // push {i} to coroutine-function
    }

##### accessing parameters

Parameters returned from or transferred to the coroutine-function can be accessed with asymmetric_coroutine<>::pull_type::get().

Splitting-up the access of parameters from context switch function enables to check if asymmetric_coroutine<>::pull_type is valid after return from asymmetric_coroutine<>::pull_type::operator(), e.g. asymmetric_coroutine<>::pull_type has values and coroutine-function has not terminated.

    boost::coroutines::asymmetric_coroutine<boost::tuple<int,int>>::push_type sink(
        [&](boost::coroutines::asymmetric_coroutine<boost::tuple<int,int>>::pull_type& source){
            // access tuple {7,11}; x==7 y==1
            int x,y;
            boost::tie(x,y)=source.get();
        });

    sink(boost::make_tuple(7,11));

##### exceptions

An exception thrown inside an asymmetric_coroutine<>::pull_type's coroutine-function before its first call to asymmetric_coroutine<>::push_type::operator() will be re-thrown by the asymmetric_coroutine<>::pull_type constructor. After an asymmetric_coroutine<>::pull_type's coroutine-function's first call to asymmetric_coroutine<>::push_type::operator(), any subsequent exception inside that coroutine-function will be re-thrown by asymmetric_coroutine<>::pull_type::operator(). asymmetric_coroutine<>::pull_type::get() does not throw.

An exception thrown inside an asymmetric_coroutine<>::push_type's coroutine-function will be re-thrown by asymmetric_coroutine<>::push_type::operator().

| Important
Code executed by coroutine-function 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::asymmetric_coroutine<void>::push_type sink(
            [&](boost::coroutines::asymmetric_coroutine<void>::pull_type& source){
                X x;
                for(int=0;;++i){
                    std::cout<<"fn(): "<<i<<std::endl;
                    // transfer execution control back to main()
                    source();
                }
            });

        sink();
        sink();
        sink();
        sink();
        sink();

        std::cout<<"sink is complete: "<<std::boolalpha<<!sink<<"\n";
    }

    output:
        X()
        fn(): 0
        fn(): 1
        fn(): 2
        fn(): 3
        fn(): 4
        fn(): 5
        sink is complete: false
        ~X()

##### Range iterators

Boost.Coroutine provides output- and input-iterators using Boost.Range. asymmetric_coroutine<>::pull_type can be used via input-iterators using std::begin() and std::end().

    int number=2,exponent=8;
    boost::coroutines::asymmetric_coroutine< int >::pull_type source(
        [&]( boost::coroutines::asymmetric_coroutine< int >::push_type & sink){
            int counter=0,result=1;
            while(counter++<exponent){
                result=result*number;
                sink(result);
            }
        });

    for (auto i:source)
        std::cout << i <<  " ";

    output:
        2 4 8 16 32 64 128 256

asymmetric_coroutine<>::pull_type::iterator::operator++() corresponds to asymmetric_coroutine<>::pull_type::operator(); asymmetric_coroutine<>::pull_type::iterator::operator*() roughly corresponds to asymmetric_coroutine<>::pull_type::get(). An iterator originally obtained from std::begin() of an asymmetric_coroutine<>::pull_type compares equal to an iterator obtained from std::end() of that same asymmetric_coroutine<>::pull_type instance when its asymmetric_coroutine<>::pull_type::operator bool would return `false`].

| Note
If `T` is a move-only type, then asymmetric_coroutine<T>::pull_type::iterator may only be dereferenced once before it is incremented again.

Output-iterators can be created from asymmetric_coroutine<>::push_type.

    boost::coroutines::asymmetric_coroutine<int>::push_type sink(
        [&](boost::coroutines::asymmetric_coroutine<int>::pull_type& source){
            while(source){
                std::cout << source.get() <<  " ";
                source();
            }
        });

    std::vector<int> v{1,1,2,3,5,8,13,21,34,55};
    std::copy(boost::begin(v),boost::end(v),boost::begin(sink));

asymmetric_coroutine<>::push_type::iterator::operator*() roughly corresponds to asymmetric_coroutine<>::push_type::operator(). An iterator originally obtained from std::begin() of an asymmetric_coroutine<>::push_type compares equal to an iterator obtained from std::end() of that same asymmetric_coroutine<>::push_type instance when its asymmetric_coroutine<>::push_type::operator bool would return `false`.

##### Exit a coroutine-function

coroutine-function is exited with a simple return statement jumping back to the calling routine. The asymmetric_coroutine<>::pull_type, asymmetric_coroutine<>::push_type becomes complete, e.g. asymmetric_coroutine<>::pull_type::operator bool, asymmetric_coroutine<>::push_type::operator bool will return `false`.

| Important
After returning from coroutine-function the coroutine is complete (can not resumed with asymmetric_coroutine<>::push_type::operator(), asymmetric_coroutine<>::pull_type::operator()).