Implementing Simple Futures with Coroutines
Instead of return
, a coroutine uses co_return
returning its result. In this post, I want to implement a simple coroutine using co_return.
You may wonder: Although I have presented the theory behind coroutines, I want to write once more about coroutines. My answer is straightforward and based on my experience. C++20 does not provide concrete coroutines. Instead, C++20 provides a framework for implementing coroutines. This framework consists of more than 20 functions, some of which you must implement and others can override. Based on these functions, the compiler generates two workflows, which define the behavior of the coroutine. To make it short. Coroutines in C++20 are double-edged swords. On one side, they give you enormous power; on the other side, they are pretty challenging to understand. I dedicated over 80 pages to coroutines in my book “C++20: Get the Details“, which has not yet explained everything.
From my experience, using simple coroutines and modifying them is the easiest – maybe only – way to understand them. And this is precisely the approach I’m pursuing in the following posts. I present simple coroutines and modify them. To make the workflow obvious, I put many comments inside and added only so much theory necessary to understand the internals of coroutines. My explanations are not complete and should only serve as a starting point to deepen your knowledge about coroutines.
A Short Reminder
While you can only call a function and return from it, you can call a coroutine, suspend and resume it, and destroy a suspended coroutine.
With the new keywords co_await
and co_yield
, C++20 extends the execution of C++ functions with two new concepts.
Thanks to co_await expression
it is possible to suspend and resume the execution of the expression. If you use co_await expression
in a function func
, the call auto getResult = func()
does not block if the function call’s result is unavailable. Instead of resource-consuming blocking, you have resource-friendly waiting.
co_yield
expression supports generator functions. The generator function returns a new value each time you call it. A generator function is a data stream from which you can pick values. The data stream can be infinite. Therefore, we are at the center of lazy evaluation with C++.
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
Additionally, a coroutine does not return
its result, a coroutine does co_return
its result.
// ... MyFuture<int> createFuture() { co_return 2021; } int main() { auto fut = createFuture(); std::cout << "fut.get(): " << fut.get() << '\n'; }
In this straightforward example createFuture
is the coroutine because it uses one of the three new keywords co_return, co_yield,
or co_await
and it returns a coroutine MyFuture<int>
. What? This is what often puzzled me. The name coroutine is used for two entities. Let me introduce two new terms. createFuture
is a coroutine factory that returns a coroutine object fut,
that can be used to ask for the result: fut.get()
.
This theory should be enough. Let’s talk about co_return
.
co_return
Admittedly, the coroutine in the following program eagerFuture.cpp
is the simplest coroutine, I can imagine that still does something meaningful: it automatically stores the result of its invocation.
// eagerFuture.cpp #include <coroutine> #include <iostream> #include <memory> template<typename T> struct MyFuture { std::shared_ptr<T> value; // (3) MyFuture(std::shared_ptr<T> p): value(p) {} ~MyFuture() { } T get() { // (10) return *value; } struct promise_type { std::shared_ptr<T> ptr = std::make_shared<T>(); // (4) ~promise_type() { } MyFuture<T> get_return_object() { // (7) return ptr; } void return_value(T v) { *ptr = v; } std::suspend_never initial_suspend() { // (5) return {}; } std::suspend_never final_suspend() noexcept { // (6) return {}; } void unhandled_exception() { std::exit(1); } }; }; MyFuture<int> createFuture() { // (1) co_return 2021; // (9) } int main() { std::cout << '\n'; auto fut = createFuture(); std::cout << "fut.get(): " << fut.get() << '\n'; // (2) std::cout << '\n'; }
MyFuture
behaves as a future, which runs immediately (see “Asynchronous Function Calls“). The call of the coroutine createFuture
(line 1) returns the future, and the call fut.get
(line 2) picks up the result of the associated promise.
There is one subtle difference to a future: the return value of the coroutine createFuture
is available after its invocation. Due to the lifetime issues of the coroutine, the coroutine is managed by a std::shared_ptr
(lines 3 and 4). The coroutine always uses std::suspend_never
(lines 5 and 6) and, therefore, neither does suspend before it runs nor after. This means the coroutine is immediately executed when the function createFuture
is invoked. The member function get_return_object
(line 7) returns the handle to the coroutine and stores it in a local variable. return_value
(lines 8) stores the result of the coroutine, which was provided by co_return 2021
(line 9). The client invokes fut.get
(line 2) and uses the future as a handle to the promise. The member function get
finally returns the result to the client (line 10).
You may think it is not worth implementing a coroutine that behaves just like a function. You are right! However, this simple coroutine is an ideal starting point for writing various implementations of futures.
At this point, I should add a bit of theory.
The Promise Workflow
When you use co_yield
, co_await
, or co_return
in a function, the function becomes a coroutine, and the compiler transforms its function body into something equivalent to the following lines.
{ Promise prom; // (1) co_await prom.initial_suspend(); // (2) try { <function body> // (3) } catch (...) { prom.unhandled_exception(); } FinalSuspend: co_await prom.final_suspend(); // (4) }
Do these function names sound familiar to you? Right! These are the member functions of the inner class promise_type
. Here are the steps the compiler performs when it creates the coroutine object as the return value of the coroutine factory createFuture
. It first creates the promise object (line 1), invokes its initial_suspend
member function (line 2), executes the body of the coroutine factory (line 3), and finally, calls the member function final_suspend
(line 4). Both member functions initial_suspend
and final_suspend
in the program eagerFuture.cpp
return the predefined awaitables std::suspend_never
. As its name suggests, this awaitable suspends never; hence, the coroutine object suspends never and behaves such as a usual function. An awaitable is something you can await on. The operator co_await needs an awaitable. I will write a post about the awaitable and the second awaiter workflow.
From this simplified promise workflow, you can deduce which member functions the promise (promise_type
) at least needs:
- A default constructor
initial_suspend
final_suspend
unhandled_exception
Admittedly, this was not the full explanation but at least enough to get the first intuition about the workflow of coroutines.
What’s next?
You may already guess it. In my next post, I will use this simple coroutine as a starting point for further experiments. First, I add comments to the program to make its workflow explicit, second, I make the coroutine lazy and resume it on another thread.
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,
Leave a Reply
Want to join the discussion?Feel free to contribute!