c++coroutinestackless

Resume ASIO Stackless Coroutine


Having played a little with the current implementation of Coroutine TS in Clang, I stumbled upon the asio stackless coroutine implementation. They are described to be Portable Stackless Coroutines in One* Header. Dealing mostly with asynchronous code I wanted to try them as well.

The coroutine block inside the main function shall await the result asynchronously set by the thread spawned in function foo. However I am uncertain on how to let execution continue at the point <1> (after the yield expression) once the thread set the value.

Using the Coroutine TS I would call the coroutine_handle, however boost::asio::coroutine seems not to be callable.

Is this even possible using boost::asio::coroutine?

#include <thread>
#include <chrono>
#include <boost/asio/coroutine.hpp>
#include <boost/asio/yield.hpp>
#include <cstdio>

using namespace std::chrono_literals;
using coroutine = boost::asio::coroutine;

void foo(coroutine & coro, int & result) {
 std::thread([&](){
  std::this_thread::sleep_for(1s);
  result = 3;
  // how to resume at <1>?
 }).detach();
}

int main(int, const char**) {
 coroutine coro;
 int result;
 reenter(coro) {
  // Wait for result
  yield foo(coro, result);
  // <1>
  std::printf("%d\n", result);
 }

 std::thread([](){
  std::this_thread::sleep_for(2s);
 }).join();
 return 0;
}

Thanks for your help


Solution

  • First off, stackless coroutines are better described as resumable functions. The problem you're currently having is using main. If you extract your logic to a separate functor it would be possible:

    class task; // Forward declare both because they should know about each other
    void foo(task &task, int &result);
    
    // Common practice is to subclass coro
    class task : coroutine {
        // All reused variables should not be local or they will be
        // re-initialized
        int result;
    
        void start() {
            // In order to actually begin, we need to "invoke ourselves"
            (*this)();
        }
    
        // Actual task implementation
        void operator()() {
            // Reenter actually manages the jumps defined by yield
            // If it's executed for the first time, it will just run from the start
            // If it reenters (aka, yield has caused it to stop and we re-execute)
            // it will jump to the right place for you
            reenter(this) {
                // Yield will store the current location, when reenter
                // is ran a second time, it will jump past yield for you
                yield foo(*this, result);
                std::printf("%d\n", result)
            }
        }
    }
    
    // Our longer task
    void foo(task & t, int & result) {
        std::thread([&](){
            std::this_thread::sleep_for(1s);
            result = 3;
            // The result is done, reenter the task which will go to just after yield
            // Keep in mind this will now run on the current thread
            t();
        }).detach();
    }
    
    int main(int, const char**) {
        task t;
    
        // This will start the task
        t.start();
    
        std::thread([](){
            std::this_thread::sleep_for(2s);
        }).join();
        return 0;
    }
    

    Note that it's not possible to yield from sub functions. This is a limitation of stackless coroutines.

    How it works:

    Now "start" is done, and you start another thread to wait for. Meanwhile, foo's thread finishes its sleep and call your task again. Now:

    foo thread is now done and main is likely still waiting for the 2nd thread.