Lazy Futures with Coroutines
Based on the coroutines-based implementation of a simple future in my last post “Implementing Simple Futures with Coroutines“, I want to go today one giant step further. I analyze the workflow of the simple future and make it lazy.
Before I create future variations, you should understand its control flow. I assume you know my previous post: “Implementing Simple Futures with Coroutines. In this post, comments help me make the coroutine’s control flow transparent. Additionally, I added a link to an online compiler to each presented program so that you directly use and experiment with the programs.
The Transparent Control Flow
// eagerFutureWithComments.cpp #include <coroutine> #include <iostream> #include <memory> template<typename T> struct MyFuture { std::shared_ptr<T> value MyFuture(std::shared_ptr<T> p): value(p) { // (3) std::cout << " MyFuture::MyFuture" << '\n'; } ~MyFuture() { std::cout << " MyFuture::~MyFuture" << '\n'; } T get() { std::cout << " MyFuture::get" << '\n'; return *value; } struct promise_type { // (4) std::shared_ptr<T> ptr = std::make_shared<T>(); // (11) promise_type() { std::cout << " promise_type::promise_type" << '\n'; } ~promise_type() { std::cout << " promise_type::~promise_type" << '\n'; } MyFuture<T> get_return_object() { std::cout << " promise_type::get_return_object" << '\n'; return ptr; } void return_value(T v) { std::cout << " promise_type::return_value" << '\n'; *ptr = v; } std::suspend_never initial_suspend() { // (6) std::cout << " promise_type::initial_suspend" << '\n'; return {}; } std::suspend_never final_suspend() noexcept { // (7) std::cout << " promise_type::final_suspend" << '\n'; return {}; } void return_void() {} void unhandled_exception() { std::exit(1); } }; // (5) }; MyFuture<int> createFuture() { // (2) std::cout << "createFuture" << '\n'; co_return 2021; } int main() { std::cout << '\n'; auto fut = createFuture(); // (1) auto res = fut.get(); // (8) std::cout << "res: " << res << '\n'; std::cout << '\n'; } // (12)
The call createFuture
(line 1) causes the creation of the instance of MyFuture
(line 2). Before MyFuture
‘s constructor call (line 3) is completed, the promise promise_type
is created, executed, and destroyed (lines 4 – 5). The promise uses the awaitable (lines 6 and 7) in each step of its control flow and, hence, never suspends. It has to be allocated to save the result of the promise for the later call (line 8). Furthermore, the used std::shared_ptr'
s ensure (lines 3 and 10) that the program does not cause a memory leak. As a local, fut
goes out of scope in line 12, and the C++ run time calls its destructor.
You can try out the program on Compiler Explorer.
The presented coroutine runs immediately and is, therefore, eager. Furthermore, the coroutine runs in the thread of the caller.
Let’s make the future lazy.
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
A Lazy Future
A lazy future is a future that runs only if asked for the value. Let’s see what I must change in the previous coroutine to make the future lazy.
// lazyFuture.cpp #include <coroutine> #include <iostream> #include <memory> template<typename T> struct MyFuture { struct promise_type; using handle_type = std::coroutine_handle<promise_type>; handle_type coro; // (5) MyFuture(handle_type h): coro(h) { std::cout << " MyFuture::MyFuture" << '\n'; } ~MyFuture() { std::cout << " MyFuture::~MyFuture" << '\n'; if ( coro ) coro.destroy(); // (8) } T get() { std::cout << " MyFuture::get" << '\n'; coro.resume(); // (6) return coro.promise().result; } struct promise_type { T result; promise_type() { std::cout << " promise_type::promise_type" << '\n'; } ~promise_type() { std::cout << " promise_type::~promise_type" << '\n'; } auto get_return_object() { // (3) std::cout << " promise_type::get_return_object" << '\n'; return MyFuture{handle_type::from_promise(*this)}; } void return_value(T v) { std::cout << " promise_type::return_value" << '\n'; result = v; } std::suspend_always initial_suspend() { // (1) std::cout << " promise_type::initial_suspend" << '\n'; return {}; } std::suspend_always final_suspend() noexcept { // (2) std::cout << " promise_type::final_suspend" << '\n'; return {}; } void return_void() {} void unhandled_exception() { std::exit(1); } }; }; MyFuture<int> createFuture() { std::cout << "createFuture" << '\n'; co_return 2021; } int main() { std::cout << '\n'; auto fut = createFuture(); // (4) auto res = fut.get(); // (7) std::cout << "res: " << res << '\n'; std::cout << '\n'; }
Let’s first study the promise. The promise always suspends at the beginning (line 1) and the end (line 2). Furthermore, the member function get_return_object
(line 3) creates the return object that is returned to the caller of the coroutine createFuture
(line 4). The future MyFuture
is more interesting. It has a handle coro
(line 5) to the promise. MyFuture
uses the handle to manage its promise. It resumes the promise (line 6), asks for the promise for the result (line 7), and finally destroys it (line 8). The resumption of the coroutine is necessary because it never runs automatically (line 1). When the client invokes fut.get()
(line 7) to ask for the result of the future, it implicitly resumes the promise (line 6).
You can try out the program on Compiler Explorer.
What happens if the client is not interested in the result of the future and, hence, does not resume the coroutine? Let’s try it out.
int main() { std::cout << '\n'; auto fut = createFuture(); // auto res = fut.get(); // std::cout << "res: " << res << '\n'; std::cout << '\n'; }
As you may guess, the promise never runs, and the member functions return_value
and final_suspend
are not executed.
Before I end this post, I want to write about the lifetime challenges of coroutines.
Lifetime Challenges of Coroutines
One of the challenges of dealing with coroutines is handling the lifetime of the coroutine.
In the first program eagerFutureWithComments.cpp
, I stored the coroutine result in a std::shared_ptr
. This is critical because the coroutine is eagerly executed.
In the program lazyFuture.cpp
, the call final_suspend
always suspends (line 2): std::suspend_always final_suspend()
. Consequently, the promise outlives the client, and a std::shared_ptr
is not necessary anymore. Returning std::suspend_never
from the function final_suspend
would cause, in this case, undefined behavior because the client would outlive the promise. Hence, the lifetime of the result
ends before the client asks for it.
What’s next?
My final step in the variation of the future is still missing. In the next post, I will resume the coroutine on a separate 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!