C++20: Coroutines with cppcoro

Contents[Show]

The cppcoro library from Lewis Baker gives you what C++20 doesn't give you: a library of C++ coroutine abstractions based on the coroutines TS.

TimelineCpp20

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 next posts to coroutines are easier to digest. I present examples of existing coroutines in cppcoro.

To make my argumentation easier, I speak from coroutines and the coroutines framework.

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 from my perspective very important because of one reason: we get not coroutines with C++20; we get a coroutines framework. This difference means, if you want to use coroutines in C++20, you are on your own. 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 extremely critical because implementing coroutines is quite challenging and, therefore, error-prone. This gap is exactly the gap that cppcoro fills. It provides abstractions for coroutine types, awaitable types, functions, cancellation, schedulers, networking, 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:

cppcoroBuild

  • -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.

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 that is 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 third is done, all other coroutines are executed. Consequentially, each coroutine takes 3 seconds.

cppcoroTask

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 one second less.

cppcoroTask2

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 type T, where values are produced lazily and synchronously.

Without further ado, the program cppcoroGenerator.cpp shows two generators in action.

 

// 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 next 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.

cppcoroGenerator

Before I end this post, I want to provide an intuition to 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: Meeting C++, Matt Braun, Roman Postanciuc, Venkata Ramesh Gudpati, Tobias Zindl, Marko, G Prvulovic, Reinhold Dröge, Abernitzke, Richard Ohnemus, Frank Grimm, Sakib, Broeserl, António Pina, Markus Falkner, Darshan Mody, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, espkk, Wolfgang Gärtner, Dendi Suhubdy, Jon Hess, Christian Wittenhorst, and Louis St-Amour.

 

 

Thanks in particular to:   crp4

 

   

Get your e-book at Leanpub:

The C++ Standard Library

 

Concurrency With Modern C++

 

Get Both as one Bundle

cover   ConcurrencyCoverFrame   bundle
With C++11, C++14, and C++17 we got a lot of new C++ libraries. In addition, the existing ones are greatly improved. The key idea of my book is to give you the necessary information to the current C++ libraries in about 200 pages. I also included more than 120 source files.  

C++11 is the first C++ standard that deals with concurrency. The story goes on with C++17 and will continue with C++20.

I'll give you a detailed insight into the current and upcoming concurrency in C++. This insight includes the theory and a lot of practice with more than 140 source files.

 

Get my books "The C++ Standard Library" (including C++17) and "Concurrency with Modern C++" in a bundle.

In sum, you get more than 700 pages full of modern C++ and more than 260 source files presenting the standard library and concurrency in practice.

 

Comments   

0 #1 Andrey 2020-04-27 12:21
Is there any example using some other function instead of fibonacci() containing co_await (calling co-routines) inside?
Quote

My Newest E-Books

Course: Modern C++ Concurrency in Practice

Course: C++ Standard Library including C++14 & C++17

Course: Embedded Programming with Modern C++

Course: Generic Programming (Templates)

Course: C++ Fundamentals for Professionals

Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 3785

All 3920395

Currently are 97 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments