C++20: More Details to Coroutines

After I gave you in my last post (C++20: Coroutines – A First Overview) my first impression of coroutines, I want to provide more details today. Once more, we get in C++20 not coroutines but a framework for building coroutines.

My job in this and further posts is to explain the framework for building coroutines. Ultimately, you can create your own or use an existing implementation of coroutines such as the excellent one cppcoro from Lewis Baker.

Today’s post is in-between: This post is not an overview but also not an in-depth dive into the coroutines framework that follows in the next posts.

The first question you may have is: When should I use coroutines?

Typical Use-Cases

Coroutines are the usual way to write event-driven applications. The event-driven application can be simulations, games, servers, user interfaces, or algorithms. For example, I wrote a a few years ago a defibrillator simulator in Python. This simulator helped us, in particular, to make clinical usability studies. A defibrillator is an event-driven application, and I implemented it, consequentially, based on the event-driven Python framework twisted.

Coroutines are also typically used for cooperative multitasking. The key to cooperative multitasking is that each task takes as much time as it needs. Cooperative multitasking stands in contrast to preemptive multitasking, for which a scheduler decides how long each task gets the CPU. Cooperative multitasking often makes concurrency easier because a concurrent job can not be interrupted in a critical region. If you are still puzzled by the terms cooperative and preemptive, I found an excellent overview and read here: Cooperative vs. Preemptive: a quest to maximize concurrency power

Underlying Ideas

Coroutines in C++20 are asymmetric, first-class, and stackless.

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (open)
  • "Generic Programming (Templates) with C++": October 2024
  • "Embedded Programming with Modern C++": October 2024
  • "Clean Code: Best Practices for Modern C++": March 2025
  • Do you want to stay informed: Subscribe.

     

    • The workflow of an asymmetric coroutine goes back to the caller.
    • First-class coroutines behave like data. Behaving like data means using them as an argument to return value from a function or store them in a variable.
    • A stackless coroutine enables it to suspend and resume the top-level coroutine. The execution of the coroutine and the yielding from the coroutine comes back to the caller. In contrast, a stackful coroutine reserves a default stack for 1MB on Windows, and 2MB
      on Linux.

    Design Goals

    Gor Nishanov, how was responsible for the standardization of coroutines in C++,  described the design goals of coroutines. Coroutines should

    • be highly scalable (to billions of concurrent coroutines).
    • have highly efficient resume and suspend operations comparable in cost to the overhead of a function.
    • seamlessly interact with existing facilities with no overhead.
    • have open-ended coroutine machinery allowing library designers to develop coroutine libraries.
    • exposing various high-level semantics such as generators, goroutines, tasks and more.
    • usable in environments where exceptions are forbidden or not available.

    Becoming a Coroutine

    A function that uses the keywords co_return, co_yield, or co_await becomes a coroutine implicitly.

    • co_return: a coroutine uses co_return as return statement.
    • co_yieldthanks to co_yield, you can implement a generator that generates an infinite data stream from which you can successively query values. The return type of the function call generatorForNumbers(int begin, int inc= 1), such as in the last post (C++20: Coroutines – A First Overview) is a generator. A generator internally holds a special promise pro so that a call co_yield i is equivalent to a call co_await pro.yield_value(i). Immediately after the call, the coroutine is paused.
    • co_await:co_await eventually causes the execution of the coroutine to be suspended or resumed. The expression exp in co_await exp has to be a so-called awaitable expression. exp has to implement a specific interface. This interface consists of three functions await_ready, await_suspend, and await_resume.

    Two Awaitables

    The C++20 standard already defines two awaitables as basic-building blocks: std::suspend_always, and std::suspend_never.

    • std::suspend_always
    struct suspend_always {
        constexpr bool await_ready() const noexcept { return false; }
        constexpr void await_suspend(coroutine_handle<>) const noexcept {}
        constexpr void await_resume() const noexcept {}
    };
    

    The awaitable std::suspend_always suspends always because await_ready returns false. The opposite holds for std::suspend_never.

    • std::suspend_never
    struct suspend_never {
        constexpr bool await_ready() const noexcept { return true; }
        constexpr void await_suspend(coroutine_handle<>) const noexcept {}
        constexpr void await_resume() const noexcept {}
    };
    

    I hope a small example helps to make the theory easier to digest. A server is the hello world example for coroutines.

    A Blocking and a Waiting Server

    A server is an event-driven application. It typically waits in an event loop for an indication event of a client.

    The following code snippet shows the structure of a straightforward server.

    Acceptor acceptor{443};               // (1)
     
    while (true){
        Socket socket= acceptor.accept(); // blocking (2)
        auto request= socket.read();      // blocking (3)
        auto response= handleRequest(request);
        socket.write(response);           // blocking (4)
    }
    

    The sequential server answers each request in the same thread. It listens on port 443 (line 1), accepts its connections (line 2), reads the incoming data from the client (line 3), and writes its answer to the client (line 4). The calls in lines 2, 3, and 4 are blocking.

    Thanks to co_await, the blocking calls can now be suspended and resumed. The resources-consuming blocking server becomes a resource-sparing waiting server.

    Acceptor acceptor{443};
    
    while (true){
        Socket socket= co_await acceptor.accept();
        auto request= co_await socket.read();
        auto response= handleRequest(request);
        co_await socket.write(response);
    }
    

    You may guess. The crucial part of understanding this coroutine is the awaitable expressions expr in the co_await expr calls. These expressions have to implement the functions await_ready, await_suspend, and await_resume.

    What’s next?

    The framework for writing a coroutine consists of more than 20 functions that you partially have to implement and partially could overwrite. My next post dives deeper into this framework.

    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)

    Do you want to stay informed about my mentoring programs? Subscribe Here

    Rainer Grimm
    Yalovastraße 20
    72108 Rottenburg

    Mobil: +49 176 5506 5086
    Mail: schulung@ModernesCpp.de
    Mentoring: www.ModernesCpp.org

    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 *