Coroutines

The concept of coroutine is nothing new, the first occurence of which dates back to the 1950s. It is an extension on the “subroutine” (or “function”) concept, supporting “suspension” (or “pause”) and “resumption” of execution between routines. A coroutine can be viewed as a kind of syntactic sugar of a state machine. Below is a simple illustration of coroutine execution in pseudo-C++ code.

 1Coro coroutine(int& value)
 2{
 3    $suspend(); // suspension point #1
 4    value = 42;
 5    $suspend(); // suspension point #2
 6    return;
 7}
 8
 9int main()
10{
11    int value = 0;
12
13    // coro runs to #1 and suspends,
14    // control flow returns to main
15    Coro coro(value);
16
17    // Resume the execution of coro,
18    // from where it was suspended (#1).
19    // It will set value to 42 and suspend again.
20    coro.$resume();
21
22    assert(value == 42);
23
24    // This time coro resumes from #2,
25    // and runs until the end (return statement).
26    coro.$resume();
27
28    return 0;
29}

In this example the control flow ping-pongs between the coroutine and the main function, which is a typical case for generator coroutines.

Generators

A generator is a lazy range of values, in which each value is generated (yielded) on demand. Many other languages have some concrete implementation of generators, for example C# functions returning IEnumerable<T> with yield return statements in the function body, or Python generators with yield statements.

C++20 only provides us with the most bare-bone API of coroutines, with all the gnarly details exposed for coroutine frame manipulations and but support from the library side for user-facing APIs. There is currently a new std::generator class template proposed for the C++23 standard, and if you don’t want to wait for another who-knows-how-many years, you can use the generator coroutine implementation in this library.

This library provides clu::generator<T> which supports yielding values from the coroutine body using the co_yield keyword. The following example shows how to use it.

 1#include <iostream>
 2#include <clu/generator.h>
 3
 4// A never-ending sequence of Fibonacci numbers
 5clu::generator<int> fibonacci()
 6{
 7    int a = 1, b = 1;
 8    while (true)
 9    {
10        co_yield a; // Yielding a value
11        a = b;
12        b = a + b;
13    }
14}
15
16int main()
17{
18    // Generators are input ranges,
19    // we can use them in range-based for loops
20    for (const int i : fibonacci())
21    {
22        std::cout << i << ' ';
23        if (i > 100)
24            return 0;
25    }
26}

For more details, see the documentation of clu::generator.

Warning

There is no documentation yet…

In the generator case, the coroutine execution is managed by the caller, since each time the caller needs another value from the generator, it resumes the coroutine from the last suspension point.

Asynchronous Tasks

A coroutine can also put its handle into some other execution context, for example, into a thread pool, such that the coroutine could be scheduled to run in parallel with other coroutines. This is the typical case for asynchronous tasks. This is typically implemented in other languages with the async-await construct.

If there were an async network library supporting coroutines, the code below would be an example of how to use it.

 1#include <awesome_coro_lib.h>
 2
 3// A coroutine that performs an asynchronous task.
 4// C++ coroutines don't need special `async` keyword,
 5// any function that contains `co_await` `co_yield` or
 6// `co_return` will be considered a coroutine.
 7task<std::string> get_response(const std::string& url)
 8{
 9    http_client client;
10    // Use the co_await keyword to suspend the coroutine,
11    // and after the request is completed, the coroutine
12    // will the resumed with the response.
13    std::string response = co_await client.get_async(url);
14    co_return response;
15}

In this example, client.get_async(url) would return an “awaitable” object. Applying co_await on the awaitable object will suspend the coroutine and wait for someone to resume. For asynchronous operations like this, the suspended coroutine’s resumption is typically registered as a “callback”, called after the asynchronous operation completes.

The clu library provides clu::task<T> to support this kind of asynchronous usage.

Note that the semantics of task here differs from that of some other languages that supports the async-await construct, such as C#. The C# Task<T> type starts eagerly, in the meaning that once a function returning Task<T> is called, the task is already running and runs parallel with the caller. This kind of detached execution means that we can no longer pass references to local variables as parameters to eager tasks, since the caller might never await on the callee task and thus might return sooner than the callee.

 1eager_task<void> callee(const std::string& str)
 2{
 3    co_await thread_pool.schedule_after(30s);
 4    // Now str is dangling...
 5    std::cout << str << '\n'; // Boom!
 6}
 7
 8eager_task<void> caller()
 9{
10    std::string message = "Hello world!";
11    callee(message);
12
13    // Now we exit the coroutine, destroying
14    // the message string, leaving a dangling
15    // reference to the callee, since callee
16    // is still waiting for the 30s delay.
17    co_return;
18}

We don’t want to pay the cost of copying the parameters to every coroutine we call, nor do we want to reference count everything. Thus we should practice the idiom of “structured-concurrency”, make sure that the callee is finished before the caller is done with it. clu::task is a lazy task type, which means that the callee is not started until the caller awaits on it.

 1clu::task<void> callee(const std::string& str)
 2{
 3    co_await thread_pool.schedule_after(30s);
 4    std::cout << str << '\n';
 5}
 6
 7clu::task<void> caller()
 8{
 9    std::string message = "Hello world!";
10
11    // Since clu::task is lazy, the following does nothing.
12    // callee(message);
13
14    co_await callee(message);
15    // Now we can safely destroy the message string.
16}