Coroutine Primer

Async programming

Asynchronous programming generally refers to a style of programming that allows tasks to be run in the background, while the other works is performed.

Imagine if you will a get-request function that performs a full http request including connecting & ssl handshakes etc.

std::string http_get(std:string_view url);

int main(int argc, char * argv[])
{
    auto res = http_get("https://boost.org");
    printf("%s", res.c_str());
    return 0;
}

The above code would be traditional synchronous programming. If we want to perform two requests in parallel we would need to create another thread to run another thread with synchronous code.

std::string http_get(std:string_view url);

int main(int argc, char * argv[])
{
    std::string other_res;

    std::thread thr{[&]{ other_res = http_get("https://cppalliance.org"); }};
    auto res = http_get("https://boost.org");
    thr.join();

    printf("%s", res.c_str());
    printf("%s", other_res.c_str());
    return 0;
}

This works, but our program will spend most of the time waiting for input. Operating systems provide APIs that allow IO to be performed asynchronously, and libraries such as boost.asio provide portable ways to manage asynchronous operations. Asio itself does not dictate a way to handle the completions. This library (boost.async) provides a way to manage this all through coroutines/awaitables.

async::promise<std::string> http_async_get(std:string_view url);

async::main co_main(int argc, char * argv[])
{
    auto [res, other_res] =
            async::join(
                http_async_get(("https://boost.org"),
                http_async_get(("https://cppalliance.org")
            );

    printf("%s", res.c_str());
    printf("%s", other_res.c_str());
    return 0;
}

In the above code the asynchronous function to perform the request takes advantage of the operating system APIs so that the actual IO doesn’t block. This means that while we’re waiting for both functions to complete, the operations are interleaved and non-blocking. At the same time async provides the coroutine primitives that keep us out of callback hell.

Coroutines

Coroutines are resumable functions. Resumable means that a function can suspend, i.e. pass the control back to the caller multiple times.

A regular function yields control back to the caller with the return function, where it also returns the value.

A coroutine on the other hand might yield control to the caller and get resumed multiple times.

A coroutine has three control keywords akin to co_return (of which only co_return has to be supported).

  • co_return

  • co_yield

  • co_await

co_return

This is similar to return, but marks the function as a coroutine.

co_await

The co_await expression suspends for an Awaitable, i.e. stops execution until the awaitable resumes it.

E.g.:

async::promise<void> delay(std::chrono::milliseconds);

async::task<void> example()
{
  co_await delay(std::chrono::milliseconds(50));
}

A co_await expression can yield a value, depending on what it is awaiting.

async::promise<std::string> read_some();

async::task<void> example()
{
  std::string res = co_await read_some();
}
In async most coroutine primitives are also Awaitables.

co_yield

The co_yield expression is similar to the co_await, but it yields control to the caller and carries a value.

For example:

async::generator<int> iota(int max)
{
  int i = 0;
  while (i < max)
    co_yield i++;

  co_return i;
}

A co_yield expression can also produce a value, which allows the user of yielding coroutine to push values into it.

async::generator<int, bool> iota()
{
  int i = 0;
  bool more = false;
  do
  {
    more = co_yield i++;
  }
  while(more);
  co_return -1;
}
Stackless

C++ coroutine are stack-less, which means they only allocate their own function frame.

See Stackless for more details.

Awaitables

Awaitables are types that can be used in a co_await expression.

struct awaitable_prototype
{
    bool await_ready();

    template<typename T>
    see_below await_suspend(std::coroutine_handle<T>);

    return_type  await_resume();
};
Type will be implicitly converted into an awaitable if there is an operator co_await call available. This documentation will use awaitable to include these types, and "actual_awaitable" to refer to type conforming to the above prototype.
Diagram

In a co_await expression the waiting coroutine will first invoke await_ready to check if the coroutine needs to suspend. When ready, it goes directly to await_resume to get the value, as there is no suspension needed. Otherwise, it will suspend itself and call await_suspend with a std::coroutine_handle to its own promise.

std::coroutine_handle<void> can be used for type erasure.

The return_type is the result type of the co_await expression, e.g. int:

int i = co_await awaitable_with_int_result();

The return type of the await_suspend can be three things:

  • void

  • bool

  • std::coroutine_handle<U>

If it is void the awaiting coroutine remains suspended. If it is bool, the value will be checked, and if falls, the awaiting coroutine will resume right away.

If a std::coroutine_handle is returned, this coroutine will be resumed. The latter allows await_suspend to return the handle passed in, being effectively the same as returning false.

If the awaiting coroutine gets re-resumed right away, i.e. after calling await_resume, it is referred to as "immediate completion" within this library. This is not to be confused with a non-suspending awaitable, i.e. one that returns true from await_ready.

Event Loops

Since the coroutines in async can co_await events, they need to be run on an event-loop. That is another piece of code is responsible for tracking outstanding event and resume a resuming coroutines that are awaiting them. This pattern is very common and is used in a similar way by node.js or python’s asyncio.

async uses an asio::io_context as its default event loop.

The event loop is accessed through an executor (following the asio terminology) and can be manually set using set_executor.