A Concise Introduction to Coroutines by Dian-Lun Lin
Today, I will start a miniseries about a scheduler for tasks. The starting point of this miniseries is a straightforward scheduler by Dian-Lun Lin that becomes increasingly sophisticated.
I have already written around 15 posts about coroutines. They explain the theory about coroutines and apply this theory in various ways. However, I still fight for an intuitive introduction to a non-trivial coroutine use case. I was, therefore, pretty happy to watch Dian-Lun Lin CppCon 2022 talk: “An Introduction to C++ Coroutines through a Thread Scheduling Demonstration“.
Today, I’m happy to present a guest post by Dian-Lun Lin. He will intuitively introduce coroutines to implement a straightforward scheduler that dispatches tasks. I will use this scheduler as a starting point for further experiments.
An Introduction to C++ Coroutines
A coroutine is a function that can suspend itself and be resumed by the caller. Unlike regular functions, which execute sequentially from start to finish, coroutines allow for controlled suspension and resumption of execution. This enables us to write code that looks synchronous but can efficiently handle asynchronous operations without blocking the calling thread.
Implementing a C++ coroutine can be a bit challenging due to its versatility. In C++ coroutines, you can fine-tune how a coroutine behaves in numerous ways. For example, you can decide whether a coroutine should be suspended when it starts or finishes, and you can precisely adjust when and where these suspensions occur within the coroutine. To illustrate, let’s begin with a straightforward example:
// simpleCoroutine.cpp #include <coroutine> #include <iostream> struct MyCoroutine { // (1) struct promise_type { MyCoroutine get_return_object() { return std::coroutine_handle<promise_type>::from_promise(*this); } std::suspend_always initial_suspend() { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void return_void() {} void unhandled_exception() {} }; MyCoroutine(std::coroutine_handle<promise_type> handle): handle{handle} {} void resume() { handle.resume(); } void destroy() { handle.destroy(); } std::coroutine_handle<promise_type> handle; }; MyCoroutine simpleCoroutine() { // (2) std::cout << "Start coroutine\n"; co_await std::suspend_always{}; std::cout << "Resume coroutine\n"; } int main() { MyCoroutine coro = simpleCoroutine(); std::cout << "Coroutine is not executed yet\n"; coro.resume(); std::cout << "Suspend coroutine\n"; coro.resume(); coro.destroy(); return 0; }
This example code demonstrates the basic usage of C++ coroutines and provides an example of a custom coroutine. To implement a C++ coroutine, you must understand four essential components: Coroutine, Promise type, Awaitable, and Coroutine handle. I will explain each component through the example code in the following sections.
Coroutine
In C++, coroutines are implemented through the co_return
, co_await
, and co_yield
keywords. These keywords allow developers to express asynchronous behavior in a structured and intuitive way. In the example coroutine, simpleCoroutine
, I call co_await std::suspend always{}
to suspend the coroutine. The std::suspend_always
is a C++ standard provided awaitable that always suspends the coroutine.
When you call the function simpleCoroutine
, it doesn’t execute the coroutine immediately. Instead, it returns a coroutine object that defines promise type
. Line (2) defines the function simpleCoroutine
that returns a MyCoroutine
object. In line (1), I define the MyCoroutine
class and the promise type. You might wonder why calling a coroutine function doesn’t immediately execute it. Again, this is because C++ Coroutine is designed to be flexible. C++ Coroutine allows you to decide when and how a coroutine should begin and end. This is defined in the promise_type
.
Promise Type
A promise_type
controls a coroutine’s behavior. Here are the key responsibilities of a promise_type
:
- Creating the Coroutine Object: The
get_return_object
function creates an instance of the coroutine and returns it to the caller. - Controlling Suspension: The
initial_suspend
andfinal_suspend
functions determine whether the coroutine should be suspended or resumed at the beginning and the end. They return awaitables that dictate how the coroutine behaves. - Handling Return Values: The
return_value
function sets the return value of the coroutine when it completes. It enables the coroutine to produce a result that the caller can retrieve. In the example code, I usereturn_void
, indicating this coroutine does not have the return value. - Handling Exceptions: The
unhandled_exception
function is invoked when an unhandled exception occurs within the coroutine. It provides a mechanism to handle exceptions gracefully.
But where is the promise_type
used? I do not see the word ”promise” in the example code. Actually, when you write your coroutine, the compiler sees your code slightly differently. The compiler’s simplified view of simpleCoroutine
is the following:
MyCoroutine simpleCoroutine() { MyCoroutine::promise_type p(); MyCoroutine coro_obj = p.get_return_object(); try { co_await p.inital_suspend(); std::cout << "Start coroutine\n"; co_await std::suspend_always{}; std::cout << "Resume coroutine\n"; } catch(...) { p.unhandled_exception(); } co_await p.final_suspend(); }
That’s why you need to define promise_type
in the MyCoroutine
class. When simpleCoroutine
is called, the compiler creates a promise_type
and calls get_return_object
() to create the MyCoroutine
object. Before the coroutine body, the compiler calls initial_suspend
to determine whether to suspend at the beginning. Finally, it calls final_suspend
to determine whether to suspend at the end. You will get compilation errors if you don’t define promise_type
and its corresponding functions.
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
Awaitable
An awaitable controls a suspension point behavior. Three functions need to be defined for an awaitable:
await_ready
: This function determines whether the coroutine can proceed without suspension. It should returntrue
if the operation is ready to continue immediately, orfalse
if suspension is required. This method is an optimization that allows you to avoid the cost of suspension in cases where it is known that the operation will be completed synchronously.await_suspend
: This function provides fine-grained control over a suspension point behavior. It passes the current coroutine handle for users to resume the coroutine later or destroy the coroutine. There are three return types for this function:void
: We suspend the coroutine. The control is immediately returned to the caller of the current coroutine.bool
: Iftrue
, we suspend the current coroutine and return control to the caller; Iffalse
, we resume the current coroutine.coroutine_handle
: We suspend the current coroutine and resume that returned coroutine handle. This is also called asymmetric transfer.
await_resume
: This function specifies what value should be returned to the coroutine when the awaited operation is complete. It resumes the coroutine’s execution, passing the awaited result. If no result is expected or needed, this function can be empty and returnvoid
.
But where are these functions used? Again, let’s look into the compiler’s view. When you call co_await std:suspend_always{}
, the compiler will transform it into the following code:
auto&& awaiter = std::suspend_always{}; if(!awaiter.await_ready()) { awaiter.await_suspend(std::coroutine_handle<>...); //<suspend/resume> } awaiter.await_resume();
That’s why you need to define all these functions. The std::suspend_always
is a C++ built-in awaitable that defines the functions as follows:
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 {} };
Coroutine Handle
Coroutine handles are used to manage the state and lifecycle of a coroutine. They provide a way to access, resume, and destroy coroutines explicitly. In the example, I call handle.resume()
to resume the coroutine and call handle.destroy()
to destroy the coroutine.
After executing the program, the results are the following:
What’s Next?
As promised, this post from Dian-Lun Lin was a concise introduction to coroutines. In the next post, Dian-Lun applies the theory to implement a single-threaded scheduler for C++ coroutines.
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, Matt Godbolt, and Honey Sukesan.
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,