C++20: More Details to Coroutines
After I gave you in my last post (C++20: Coroutines – A First Overview) my first impression of coroutines, I want to provide more details today. Once more, we get in C++20 not coroutines but a framework for building coroutines.
My job in this and further posts is to explain the framework for building coroutines. Ultimately, you can create your own or use an existing implementation of coroutines such as the excellent one cppcoro from Lewis Baker.
Today’s post is in-between: This post is not an overview but also not an in-depth dive into the coroutines framework that follows in the next posts.
The first question you may have is: When should I use coroutines?
Typical Use-Cases
Coroutines are the usual way to write event-driven applications. The event-driven application can be simulations, games, servers, user interfaces, or algorithms. For example, I wrote a a few years ago a defibrillator simulator in Python. This simulator helped us, in particular, to make clinical usability studies. A defibrillator is an event-driven application, and I implemented it, consequentially, based on the event-driven Python framework twisted.
Coroutines are also typically used for cooperative multitasking. The key to cooperative multitasking is that each task takes as much time as it needs. Cooperative multitasking stands in contrast to preemptive multitasking, for which a scheduler decides how long each task gets the CPU. Cooperative multitasking often makes concurrency easier because a concurrent job can not be interrupted in a critical region. If you are still puzzled by the terms cooperative and preemptive, I found an excellent overview and read here: Cooperative vs. Preemptive: a quest to maximize concurrency power
Underlying Ideas
Coroutines in C++20 are asymmetric, first-class, and stackless.
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
- The workflow of an asymmetric coroutine goes back to the caller.
- First-class coroutines behave like data. Behaving like data means using them as an argument to return value from a function or store them in a variable.
- A stackless coroutine enables it to suspend and resume the top-level coroutine. The execution of the coroutine and the yielding from the coroutine comes back to the caller. In contrast, a stackful coroutine reserves a default stack for 1MB on Windows, and 2MB
on Linux.
Design Goals
Gor Nishanov, how was responsible for the standardization of coroutines in C++, described the design goals of coroutines. Coroutines should
- be highly scalable (to billions of concurrent coroutines).
- have highly efficient resume and suspend operations comparable in cost to the overhead of a function.
- seamlessly interact with existing facilities with no overhead.
- have open-ended coroutine machinery allowing library designers to develop coroutine libraries.
- exposing various high-level semantics such as generators, goroutines, tasks and more.
- usable in environments where exceptions are forbidden or not available.
Becoming a Coroutine
A function that uses the keywords co_return
, co_yield,
or co_await
becomes a coroutine implicitly.
- co_return: a coroutine uses co_return as return statement.
- co_yield: thanks to co_yield, you can implement a generator that generates an infinite data stream from which you can successively query values. The return type of the function call generatorForNumbers(int begin, int inc= 1), such as in the last post (C++20: Coroutines – A First Overview) is a generator. A generator internally holds a special promise pro so that a call co_yield i is equivalent to a call co_await pro.yield_value(i). Immediately after the call, the coroutine is paused.
- co_await:co_await eventually causes the execution of the coroutine to be suspended or resumed. The expression exp in co_await exp has to be a so-called awaitable expression. exp has to implement a specific interface. This interface consists of three functions await_ready, await_suspend, and await_resume.
Two Awaitables
The C++20 standard already defines two awaitables as basic-building blocks: std::suspend_always, and std::suspend_never.
- std::suspend_always
struct suspend_always { constexpr bool await_ready() const noexcept { return false; } constexpr void await_suspend(coroutine_handle<>) const noexcept {} constexpr void await_resume() const noexcept {} };
The awaitable std::suspend_always suspends always because await_ready returns false. The opposite holds for std::suspend_never.
- std::suspend_never
struct suspend_never {
constexpr bool await_ready() const noexcept { return true; } constexpr void await_suspend(coroutine_handle<>) const noexcept {} constexpr void await_resume() const noexcept {} };
I hope a small example helps to make the theory easier to digest. A server is the hello world example for coroutines.
A Blocking and a Waiting Server
A server is an event-driven application. It typically waits in an event loop for an indication event of a client.
The following code snippet shows the structure of a straightforward server.
Acceptor acceptor{443}; // (1) while (true){ Socket socket= acceptor.accept(); // blocking (2) auto request= socket.read(); // blocking (3) auto response= handleRequest(request); socket.write(response); // blocking (4) }
The sequential server answers each request in the same thread. It listens on port 443 (line 1), accepts its connections (line 2), reads the incoming data from the client (line 3), and writes its answer to the client (line 4). The calls in lines 2, 3, and 4 are blocking.
Thanks to co_await, the blocking calls can now be suspended and resumed. The resources-consuming blocking server becomes a resource-sparing waiting server.
Acceptor acceptor{443}; while (true){ Socket socket= co_await acceptor.accept(); auto request= co_await socket.read(); auto response= handleRequest(request); co_await socket.write(response); }
You may guess. The crucial part of understanding this coroutine is the awaitable expressions expr in the co_await expr calls. These expressions have to implement the functions await_ready, await_suspend, and await_resume.
What’s next?
The framework for writing a coroutine consists of more than 20 functions that you partially have to implement and partially could overwrite. My next post dives deeper into this framework.
Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery,and Matt Godbolt.
Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.
My special thanks to Embarcadero | |
My special thanks to PVS-Studio | |
My special thanks to Tipi.build | |
My special thanks to Take Up Code | |
My special thanks to SHAVEDYAKS |
Modernes C++ GmbH
Modernes C++ Mentoring (English)
Rainer Grimm
Yalovastraße 20
72108 Rottenburg
Mail: schulung@ModernesCpp.de
Mentoring: www.ModernesCpp.org
Modernes C++ Mentoring,
Leave a Reply
Want to join the discussion?Feel free to contribute!