Resume ASIO Stackless Coroutine

1.3k views Asked by At

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

1

There are 1 answers

0
Daniël Sonck On BEST ANSWER

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:

  • yield stores a unique identifier to jump to inside the coroutine
  • yield will run the expression you put behind it, should be an async call or little benefit would be given
  • after running, it will break out of the reenter block.

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:

  • the reenter block will read the state of your coroutine, to find it has to jump past the foo call
  • your task will resume, print the result and drop out of the function, returning to the foo thread.

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