C++20: Coroutines with cppcoro
The cppcoro library from Lewis Baker gives you what C++20 doesn’t: a library of C++ coroutine abstractions based on the coroutines TS.
Arguably, the two last posts, “C++20: An Infinite Data Stream with Coroutines” and “C++20: Thread Synchronization with Coroutines” were challenging to comprehend. The following posts to coroutines are easier to digest. I present examples of existing coroutines in cppcoro.
I speak from the coroutines and the coroutines framework to make my argument easier.
cppcoro
The coroutine TS is the base of the cppcoro library from Lewis Baker. TS stands for technical specifications and is the preliminary version of the coroutines framework we get with C++20. Lewis will port the cppcoro library from the coroutines TS framework to the coroutines framework we get with C++20.
Porting the library is very important because we get not coroutines with C++20; we get a coroutines framework. This difference means you are on your own if you want to use coroutines in C++20. You have to create your coroutines based on the C++20 coroutines framework. Presumably, we get concrete coroutines with C++23. Honestly, I see this as extremely critical because implementing coroutines is quite challenging and error-prone. This gap is precisely the gap that cppcoro fills. It provides abstractions for coroutine types, awaitable types, functions, cancellation, schedulers, networking, and metafunctions, and defines a few concepts.
Using cppcoro
Currently, cppcoro is based on the coroutines TS frameworks and can be used on Windows (Visual Studio 2017) or Linux (Clang 5.0/6.0 and libc++). For your experiments, I used the following command line for all of the examples:
- -std=c++17: support for C++17
- -fcoroutines-ts: support for C++ coroutines TS
- -Iinclude: cppcoro headers
- -stdlib=libc++: LLVM implementation of the standard library
- libcppcoro.a: cppcoro library
As I already mentioned: when cppcoro is in the future on C++20, you can use it with each compiler that supports C++20. Additionally, they give you a flavor for the concrete coroutines we may get with C++23.
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
After a steep learning curve, I want to show you a few examples to cppcoro. I use existing code snippets or dig into the tests to present the various features of cppcoro. Let’s start with the coroutine types.
Coroutines Types
cppcoro has various kinds of tasks and generators.
task<T>
What is a task? Here is the definition, directly stolen from the documentation.
- A task represents an asynchronous computation executed lazily in that the execution of the coroutine does not start until the task is awaited.
A task is a coroutine. In the following program, the function main waits for the function first, first waits for second, and second waits for the third.
// cppcoroTask.cpp #include <chrono> #include <iostream> #include <string> #include <thread> #include <cppcoro/sync_wait.hpp> #include <cppcoro/task.hpp> using std::chrono::high_resolution_clock; using std::chrono::time_point; using std::chrono::duration; using namespace std::chrono_literals; // 1s auto getTimeSince(const time_point<high_resolution_clock>& start) { auto end = high_resolution_clock::now(); duration<double> elapsed = end - start; return elapsed.count(); } cppcoro::task<> third(const time_point<high_resolution_clock>& start) { std::this_thread::sleep_for(1s); std::cout << "Third waited " << getTimeSince(start) << " seconds." << std::endl; co_return; // (4) } cppcoro::task<> second(const time_point<high_resolution_clock>& start) { auto thi = third(start); // (2) std::this_thread::sleep_for(1s); co_await thi; // (3) std::cout << "Second waited " << getTimeSince(start) << " seconds." << std::endl; } cppcoro::task<> first(const time_point<high_resolution_clock>& start) { auto sec = second(start); // (2) std::this_thread::sleep_for(1s); co_await sec; // (3) std::cout << "First waited " << getTimeSince(start) << " seconds." << std::endl; } int main() { std::cout << std::endl; auto start = high_resolution_clock::now(); cppcoro::sync_wait(first(start)); // (1) std::cout << "Main waited " << getTimeSince(start) << " seconds." << std::endl; std::cout << std::endl; }
Admittedly, the program doesn’t do something meaningful, but it helps to understand the workflow of coroutines.
First of all, the main function can’t be a coroutine. cppcoro::sync_wait (line 1) often serves, such as in this case, as a starting top-level task and waits until the task finishes. The coroutine first, such as all other coroutines, gets as an argument the start time and displays its execution time. What happens in the coroutine first? It starts the coroutine second (line 2), which is immediately paused, sleeps for a second, and resumes the coroutine via its handle sec in line (3). The coroutine second follows the same workflow but not the coroutine third. third is a coroutine that returns nothing and does not wait on another coroutine. When the third is done, all other coroutines are executed. Consequentially, each coroutine takes 3 seconds.
Let’s vary the program a little. What happens if the coroutines sleep after the co_await call?
// cppcoroTask2.cpp #include <chrono> #include <iostream> #include <string> #include <thread> #include <cppcoro/sync_wait.hpp> #include <cppcoro/task.hpp> using std::chrono::high_resolution_clock; using std::chrono::time_point; using std::chrono::duration; using namespace std::chrono_literals; auto getTimeSince(const time_point<::high_resolution_clock>& start) { auto end = high_resolution_clock::now(); duration<double> elapsed = end - start; return elapsed.count(); } cppcoro::task<> third(const time_point<high_resolution_clock>& start) { std::cout << "Third waited " << getTimeSince(start) << " seconds." << std::endl; std::this_thread::sleep_for(1s); co_return; } cppcoro::task<> second(const time_point<high_resolution_clock>& start) { auto thi = third(start); co_await thi; std::cout << "Second waited " << getTimeSince(start) << " seconds." << std::endl; std::this_thread::sleep_for(1s); } cppcoro::task<> first(const time_point<high_resolution_clock>& start) { auto sec = second(start); co_await sec; std::cout << "First waited " << getTimeSince(start) << " seconds." << std::endl; std::this_thread::sleep_for(1s); } int main() { std::cout << std::endl; auto start = ::high_resolution_clock::now(); cppcoro::sync_wait(first(start)); std::cout << "Main waited " << getTimeSince(start) << " seconds." << std::endl; std::cout << std::endl; }
You may have guessed it. The main function waits 3 seconds, but each iteratively invoked coroutine is one second less.
In further posts, I add threads and signals to tasks.
generator<T>
Here is the definition from cppcoro.
- A
generator
represents a coroutine type that produces a sequence of values of typeT
, where values are produced lazily and synchronously.
Without further ado, the program cppcoroGenerator.cpp shows two generators.
// cppcoroGenerator.cpp #include <iostream> #include <cppcoro/generator.hpp> cppcoro::generator<char> hello() { co_yield 'h'; co_yield 'e'; co_yield 'l'; co_yield 'l'; co_yield 'o'; } cppcoro::generator<const long long> fibonacci() { long long a = 0; long long b = 1; while (true) { co_yield b; // (2) auto tmp = a; a = b; b += tmp; } } int main() { std::cout << std::endl; for (auto c: hello()) std::cout << c; std::cout << "\n\n"; for (auto i: fibonacci()) { // (1) if (i > 1'000'000) break; std::cout << i << " "; } std::cout << "\n\n"; }
The first coroutine hello returns on request the next character; the coroutine Fibonacci the following Fibonacci number. Fibonacci creates an infinite data stream. What happens in line 1? The range-based for-loop triggers the execution of the coroutine. The first iteration starts the coroutines, returns the value at co_yield b, and pauses. Subsequent calls of the range-based for-loop resume the coroutine fibonacci and return the next Fibonacci number.
Before I end this post, I want to provide an intuition of the difference between co_await (task) and co_yield (generator): co_await waits to the inside, co_yield waits to the outside. For example, the coroutine first waits for the called coroutine second (cppcoroTask.cpp), but the coroutine fibonacci (cppcoroGenerator.cpp) is triggered by the external range-based for-loop.
What’s next?
My next post to cppcoro dives deeper into tasks. I combine them with threads, signals, or thread pools.
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!