A C++20 coroutine example

One of the most important new features in the C++20 is coroutines. A coroutine is a function that has the ability to be suspended and resumed. A function becomes a coroutine if it uses any of the following:

  • the co_await operator to suspend execution until resumed
  • the co_return keyword to complete execution and optionally return a value
  • the co_yield keyword to suspend execution and return a value

A coroutine must also have a return type that satisfies some requirements. However, the C++20 standard, only defines a framework for the execution of coroutines, but does not define any coroutine types satisfying such requirements. That means, we need to either write our own or rely on 3rd party libraries for this. In this post, I’ll show how to write some simple examples using the cppcoro library.

The cppcoro library contains abstractions for the C++20 coroutines, including task, generator, and async_generator. A task represents an asynchronous computation that is executed lazily (that means only when the coroutine is awaited) and a generator is a sequence of values of some T type, that are produced lazily (that is, when the begin() function is called to retrieve an iterator or the ++ operator is called on the iterator).

Let us look at an example. The function produce_items() below is a coroutine, because it uses the co_yield keyword to return a value and has the return type cppcoro::generator<std::string> that satisfies the requirements of a generator coroutine.

NOTE: the use of the rand() function is for simplicity only. Do not use this obsolete function for production code.

This function has an infinite loop, but the execution is suspended when the co_yield statement executes. This function produces a random number each time it is resumed. This happens when the generator is being iterated. And example is shown below:

The consume_items function is also a coroutine. It uses the co_return keyword to complete execution and its return type is cppcodo::task<>, which also satisfies the requirements for a coroutine type. This function runs a loop n times using a range-based for loop. This loop calls the begin() function of the cppcoro::generator<std::string> class and retrieves an iterator that is later incremented with operator++. The produce_items() is resumed upon each of these calls and returns a new (random) value. If an exception occurs, it is re-throwned to the caller from the invocation of begin() or operator++. The produce_items() function could be resumed indefinitely, although the consuming code only does so for a finite number of times.

The consume_items() can be invoked from the main() function. However, because main() cannot be a coroutine, it cannot use the co_await operator to await for the completion of its execution. To help with that, the cppcoro library provides a function called sync_wait() that synchronously waits until the specified awaitable completes (which is awaited on the current thread inside a newly created coroutine). This function blocks the current thread until the operation completes and returns the result of the co_await expression. In an exception occurs, it is rethrown to the caller.

The following snipper shows how we can invoke and wait for consume_items() from main():

The output from running this program is as follows:

The cppcoro::generator<T> produces values in a lazy but synchronously way. That means, using the co_await operator from a coroutine returning this type is not possible. However, the cppcoro library features an asynchronous generator, called cppcoro::async_generator<T>, that makes this possible.

We can change the preceding example as follows: a new coroutine, next_value() returns a value that takes some time to be computed. We simulate that by awaiting for a random number of seconds. The produce_items() coroutine waits for a new value in each loop and then returns a new item from that value. The return type, this time, is cppcoro::async_generator<T>.

The consumer requires a slight change, because it has to await for each new value. This is done with the use of the co_await operator in the for loop as follows:

The co_return statement is no longer present in this implementation, although it could be added. Because the co_await is used in the for loop, the function is coroutine. You do not need to add empty co_return statements at the end of a coroutine returning cppcoro::task<>, just like you don’t need empty return statements at the end of a regular function returning void. The previous implementation required this statement because there was no call to co_await, therefore, the co_return was necessary to make the function a coroutine.

There are no changes required to the main() function. However, when we execute the code this time, each value is produced after some random time interval, as the following image shows:

For the sake of completeness, the print_time() function referred in these snippets is as follows:

Another important thing to note here, is that invoking co_await with a time duration is not possible out of the box. However, it is made possible by overloading the co_await operator. An implementation that works on Windows is the following:

This implementation has been sourced from the article Coroutines in Visual Studio 2015 – Update 1.
UPDATE: The code has been changed based on the feedback. See the comments section.

To learn more about coroutines see:

5 Replies to “A C++20 coroutine example”

  1. Be careful when using old code like this one (it’s from a 5-year-old blog post). It has a subtle but insidious bug in it, which I’ve seen strike unexpectedly in multiple places, with experts puzzling about it in long email threads. The rationale is well explained here by Raymond Chen: https://devblogs.microsoft.com/oldnewthing/20191209-00/?p=103195

    But let me elaborate my own take on it.

    The call to “SetThreadpoolTimer” effectively sends the entire coroutine (by sending its handle) on its merry way to be resumed (and eventually destroyed) in another thread. This has deep implications to what is permissible in the remainder of await_suspend. In particular, you can’t use *this anymore (in a way similar to how you can’t use *this after using the “delete this” pattern).

    In this code “return timer != 0;” is using *this (timer is a member of awaiter), at a time when it’s very possible for awaiter to be destroyed (as soon as the coroutine is resumed), and even for the entire coroutine frame to be destroyed and freed, if the coroutine happens to run to completion quickly enough.

    The solution here is very simple, because await_suspend doesn’t really need the timer there. It just needs to know whether the call to CreateThreadpoolTimer succeeded, and that can easily be determined before calling SetThreadpoolTimer.

    But I’ve seen multiple cases where this is more awkward to fix, usually involving richer async-aware frameworks like PPL or WinRT. In those cases, the “sending the handle on its merry way” is deeper in a stack of function calls, and it is imperative that the entire coroutine frame (including all locals and temporaries of the coroutine function, as well as the promise object and everything that’s “owned” by them) be untouched as those functions complete their work (including local destructors) and return.

    Same thing, BTW, in the TimerCallback function. As soon as “stdco::coroutine_handle::from_address(Context).resume()” is invoked, the coroutine can be considered to have been sent on its merry way. await_resume will be called synchronously from there, but after that the coroutine may co_await again, thus calling await_suspend and sending the handle on to yet another thread, all within that call to resume().

    Crafting asynchronous-aware awaitables/awaiters and promises is delicate work, that is certainly true, wouldn’t you say? It shouldn’t be done lightly :-).

  2. It’s just serendipity, to be honest. I had just happened to get involved in a discussion at work, where this issue had shown up in a test run and we were discussing the cause and possible solutions. It’s amazing how we all (including people smarter than me or probably you, if I can say this) can lose track of this constraint for a second and introduce the subtle race condition.

    The product I work on (https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/time-travel-debugging-overview) uses coroutines quite extensively, so we’ve had to deal with everything from compiler bugs to framework bugs to our own silliness, so by now I think I’m beginning to really get it (famous last words :)).

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.