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.

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

Be part of my mentoring programs:

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (starts March 2024)
  • Do you want to stay informed: Subscribe.

     

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

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

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

    cppcoroGenerator

    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, Kris Kafka, 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, Matthieu Bolt, Stephen Kelley, Kyle Dean, Tusar Palauri, Dmitry Farberov, 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, moon, and Philipp Lenk.

    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

    Seminars

    I’m happy to give online seminars or face-to-face seminars worldwide. Please call me if you have any questions.

    Standard Seminars (English/German)

    Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.

    • C++ – The Core Language
    • C++ – The Standard Library
    • C++ – Compact
    • C++11 and C++14
    • Concurrency with Modern C++
    • Design Pattern and Architectural Pattern with C++
    • Embedded Programming with Modern C++
    • Generic Programming (Templates) with C++
    • Clean Code with Modern C++
    • C++20

    Online Seminars (German)

    Contact Me

    Modernes C++ Mentoring,

     

     

    0 replies

    Leave a Reply

    Want to join the discussion?
    Feel free to contribute!

    Leave a Reply

    Your email address will not be published. Required fields are marked *