Coroutines¶

  • A coroutine in C++ refers to a stackless coroutine
    • Sometimes called a resumable function
    • Defined in the C++ Extensions for Coroutines Technical Specification
    • Approved for C++20
  • Coroutines can halt execution
    • yielding a value (or void)
    • or awaiting a value (or event)
  • Once halted, a coroutine can be resumed, or destructed
  • Coroutine is any function which
    • is not main()
    • is not a constructor
    • is not destructor
    • result type is not auto
    • contains a co_return statement
    • a co_await expression
    • a range based for loop with co_await
    • a co_yield expression
    • does not contain variable arguments (parameter packs are allowed)
      • i.e. printf(const char*, ...); // not allowed

Anatomy of a Coroutine¶

  • A simple example
generator my_coroutine() {
    int n = 0;
    while (true) {
        co_yield n++;
    }
}

int main() {
    generator x = my_coroutine();
    cout << x.get() << endl;
    cout << x.get() << endl;
    cout << x.get() << endl;
}
0
1
2
Program ended with exit code: 0
  • A coroutine is a function object with multiple entry points
    • Manually written:
namespace v0 {

struct my_coroutine_t {
    // ...
};

} // namespace v0
  • Local variables and arguments are captured within the coroutine
namespace v1 {

struct my_coroutine_t {
    int n = 0;
    // ...
};

} // namespace v1
  • On construction, a coroutine may either be suspended or start executing
    • suspension is handled by setting a resume point and returning
namespace v2 {

struct my_coroutine_t {
    int n = 0;

    void (my_coroutine_t::*_resume)();

    my_coroutine_t() : _resume{&my_coroutine_t::state_01} {}

    void resume() { (this->*_resume)(); }

    void state_01(); //...
};

} // namespace v2
  • The resume location will execute to the first yield or await and then return
    • yielding is handled by setting a promise
namespace {

struct my_coroutine_t {
    int n = 0;

    void (my_coroutine_t::*_resume)();
    int _promise;

    my_coroutine_t() : _resume{&my_coroutine_t::state_01} {}

    void resume() { (this->*_resume)(); }

    void state_01() {
        _promise = n++;                      // co_yield n++
        _resume = &my_coroutine_t::state_01; // on resume, loop
    }
};

} // namespace v3
  • Calling a coroutine allocates and constructs the coroutine and returns an object constructed with the coroutine handle
namespace v3 {

using coroutine_handle = unique_ptr<my_coroutine_t>;

struct generator {
    coroutine_handle _handle;
    generator(coroutine_handle h) : _handle(move(h)) {}
    // ...
};

generator my_coroutine() { return generator(make_unique<my_coroutine_t>()); }

} // namespace v3
  • The coroutine result type can be used to drive the coroutine
namespace v4 {

struct generator {
    coroutine_handle _handle;
    generator(coroutine_handle h) : _handle(move(h)) {}

    int get() {
        _handle->resume();
        return _handle->_promise;
    }
};

} // namespace v3
  • Now we can use our coroutine
generator x = my_coroutine();
cout << x.get() << endl;
cout << x.get() << endl;
cout << x.get() << endl;
  • The generator type used for the C++TS version is declared as:
struct generator {
    struct promise_type;
    using handle = coroutine_handle<promise_type>;

    struct promise_type {
        int current_value;

        auto initial_suspend() { return suspend_always{}; }
        auto final_suspend() { return suspend_always{}; }

        void unhandled_exception() { terminate(); }
        void return_void() {}
        auto yield_value(int value) {
            current_value = value;
            return suspend_always{};
        }
        generator get_return_object() {
            return generator{handle::from_promise(*this)};
        }
    };
    handle _coro;

    generator(handle h) : _coro(h) {}
    generator(generator const&) = delete;
    generator(generator&& rhs) : _coro(rhs._coro) { rhs._coro = nullptr; }
    ~generator() {
        if (_coro) _coro.destroy();
    }

    int get() {
        _coro.resume();
        return _coro.promise().current_value;
    }
};

Await¶

  • Besides yielding values a coroutine can also await a value
    • a co_await expression will suspend the coroutine until resume is called after a value is available
    • phrased another way, an awaiting coroutine is a continuation
future<void> do_it(future<int> x) {
    int result = co_await move(x);
    cout << result << endl;
    co_return;
}

auto done = do_it(async(default_executor, []{ return 42; }));
done.then([]{ cout << "done" << endl; });
42
done
  • Using C++ coroutines without a library is cumbersome
    • They provide a tremendous amount of power for library writers
    • Coroutines have many applications
      • range algorithms
      • concurrency and tasking
      • generators and consumers
      • state machines
    • Lambdas can also be coroutines
    • The hope is that we have some good, basic, library constructs for C++20

Homework¶

  • Rewrite sequential_process as a coroutine
    • You may use C++TS coroutines
    • But it is probably simpler to code the coroutine by hand
    • Assume a single threaded system
      • Don't worry about syncronization
      • Bonus points for trying
    • Use std::future<> for task results