Starting Jobs with Coroutines
C++20 has three new keywords to make a coroutine out of a function: co_return
, co_yield
, and co_await
. co_await
requires an Awaitable as arguments and starts the Awaiter workflow. Let me show in this post what that means.
To understand this post, you should have a basic understanding of coroutines. Here are my previous posts to coroutines, presenting coroutines from the practical perspective.
co_return
:
- Implementing Simple Futures With Coroutines
- Lazy Futures with Coroutines
- Executing a Future in a separate Thread with Coroutines
co_yield:
Before implementing Awaitables and showing their applications, I should write about the awaiter workflow.
The Awaiter Workflow
First, I have a short reminder. The awaiter workflow is based on the member functions of the Awaitable: await_ready()
, await_suspend()
, and await_resume().
C++20 has the two predefined Awaitables std::suspend_always
and std::suspend_never
, which I heavily used in this mini-series to coroutines.
std::suspend_always
struct suspend_always { constexpr bool await_ready() const noexcept { return false; } constexpr void await_suspend(std::coroutine_handle<>) const noexcept {} constexpr void await_resume() const noexcept {} };
std::suspend_never
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
struct suspend_never { constexpr bool await_ready() const noexcept { return true; } constexpr void await_suspend(std::coroutine_handle<>) const noexcept {} constexpr void await_resume() const noexcept {} };
Here is the awaiter workflow in prose.
awaitable.await_ready() returns false: // (1) suspend coroutine awaitable.await_suspend(coroutineHandle) returns: // (3) void: // (4) awaitable.await_suspend(coroutineHandle); coroutine keeps suspended return to caller bool: // (5) bool result = awaitable.await_suspend(coroutineHandle); if result: coroutine keep suspended return to caller else: go to resumptionPoint another coroutine handle: // (6) auto anotherCoroutineHandle = awaitable.await_suspend(coroutineHandle); anotherCoroutineHandle.resume(); return to caller resumptionPoint: return awaitable.await_resume(); // (2)
The workflow is only executed if awaitable.await_ready()
returns false
(line 1). In case it returns true
, the coroutine is ready and returns with the result of the call awaitable.await_resume()
(line 2).
Let me assume that awaitable.await_ready()
returns false
. First, the coroutine is suspended (line 3), and the return value is immediately evaluated. The return type can be void
(line 4), a boolean (line 5), or another coroutine handle (line 6), such as anotherCoroutineHandle.
Depending on the return type, the program flow returns, or another coroutine is executed.
Let me apply the theory and start a job on request.
Starting a Job on Request
The coroutine in the following example is as simple as it can be. It awaits on the predefined Awaitable std::suspend_never()
.
// startJob.cpp #include <coroutine> #include <iostream> struct Job { struct promise_type; using handle_type = std::coroutine_handle<promise_type>; handle_type coro; Job(handle_type h): coro(h){} ~Job() { if ( coro ) coro.destroy(); } void start() { coro.resume(); // (6) } struct promise_type { auto get_return_object() { return Job{handle_type::from_promise(*this)}; } std::suspend_always initial_suspend() { // (4) std::cout << " Preparing job" << '\n'; return {}; } std::suspend_always final_suspend() noexcept { // (7) std::cout << " Performing job" << '\n'; return {}; } void return_void() {} void unhandled_exception() {} }; }; Job prepareJob() { // (1) co_await std::suspend_never(); // (2) } int main() { std::cout << "Before job" << '\n'; auto job = prepareJob(); // (3) job.start(); // (5) std::cout << "After job" << '\n'; }
You may think that the coroutine prepareJob
(line 1) is meaningless because the Awaitable always suspends. No! The function prepareJob
is at least a coroutine factory using co_await
(line 2) and returning a coroutine object. The function call prepareJob()
in line 3 creates the coroutine object of type Job
. When you study the data type Job, you recognize that the coroutine object is immediately suspended because the member function of the promise returns the Awaitable std::suspend_always
(line 5). This is precisely why the function call job.start
(line 5) is necessary to resume the coroutine (line 6). The member function final_suspend
() also returns std::suspend_always
(line 27).
The program startJob.cpp
is an ideal starting point for further experiments. First, making the workflow transparent eases its understanding.
The Transparent Awaiter Workflow
I added a few comments to the previous program.
// startJobWithComments.cpp #include <coroutine> #include <iostream> struct MySuspendAlways { // (1) bool await_ready() const noexcept { std::cout << " MySuspendAlways::await_ready" << '\n'; return false; } void await_suspend(std::coroutine_handle<>) const noexcept { std::cout << " MySuspendAlways::await_suspend" << '\n'; } void await_resume() const noexcept { std::cout << " MySuspendAlways::await_resume" << '\n'; } }; struct MySuspendNever { // (2) bool await_ready() const noexcept { std::cout << " MySuspendNever::await_ready" << '\n'; return true; } void await_suspend(std::coroutine_handle<>) const noexcept { std::cout << " MySuspendNever::await_suspend" << '\n'; } void await_resume() const noexcept { std::cout << " MySuspendNever::await_resume" << '\n'; } }; struct Job { struct promise_type; using handle_type = std::coroutine_handle<promise_type>; handle_type coro; Job(handle_type h): coro(h){} ~Job() { if ( coro ) coro.destroy(); } void start() { coro.resume(); } struct promise_type { auto get_return_object() { return Job{handle_type::from_promise(*this)}; } MySuspendAlways initial_suspend() { // (3) std::cout << " Job prepared" << '\n'; return {}; } MySuspendAlways final_suspend() noexcept { // (4) std::cout << " Job finished" << '\n'; return {}; } void return_void() {} void unhandled_exception() {} }; }; Job prepareJob() { co_await MySuspendNever(); // (5) } int main() { std::cout << "Before job" << '\n'; auto job = prepareJob(); // (6) job.start(); // (7) std::cout << "After job" << '\n'; }
First, I replaced the predefined Awaitables std::suspend_always
and std::suspend_never
with Awaitables MySuspendAlways
(line 1) and MySuspendNever
(line 2). I use them in lines 3, 4, and 5. The Awaitables mimic the behavior of the predefined Awaitables but additionally write a comment. Due to the use of std::cout
, the member functions await_ready
, await_suspend
, and await_resume
cannot be declared as constexpr
.
The screenshot of the program execution shows the control flow nicely, which you can observe on the Compiler Explorer.
The function initial_suspend
(line 3) is executed at the beginning of the coroutine and the function final_suspend
at its end (line 4). The call prepareJob()
(line 6) triggers the creation of the coroutine object, and the function call job.start()
its resumption and, hence, completion (line 7). Consequently, the members await_ready
, await_suspend
, and await_resume
of MySuspendAlways
are executed. When you don’t resume the Awaitable, such as the coroutine object returned by the member function final_suspend
, the function await_resume
is not processed. In contrast, the Awaitable’s MySuspendNever
the function is immediately ready because await_ready
returns true
and, hence, does not suspend.
What’s next?
In my next posts, I automatically resume the Awaiter on the same and, finally, 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!