C++20: More Details to Coroutines

Contents[Show]

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

TimelineCpp20

My job in this and further posts is to explain the framework for building coroutines. On the end, you can create your own or using 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 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 even algorithms. For example, I wrote a few years ago a simulator for a defibrillator 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 we have a scheduler that decides how long each task gets the CPU. Cooperative multitasking makes concurrency often easier because a concurrent job can not be interrupted in a critical region. If you are still puzzled with 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.

  • The workflow of an asymmetric coroutine goes back to the caller. 
  • First-class coroutines behave like data. Behaving like data means you can use them as an argument to or 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 the 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 of 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, therefore, 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 to understand this coroutine are 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, Marko, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Darshan Mody, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, espkk, Wolfgang Gärtner,  Louis St-Amour, Stephan Roslen, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Avi Kohn, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Neil Wang, Friedrich Huber, Sudhakar Balagurusamy, lennonli, and Pramod Tikare Muralidhara.

 

Thanks in particular to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, and Dendi Suhubdy

 

Seminars

I'm happy to give online-seminars or face-to-face seminars world-wide. Please call me if you have any questions.

Bookable (Online)

Deutsch

English

Standard Seminars 

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

New

Contact Me

Modernes C++,

RainerGrimmSmall

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 90

Yesterday 8242

Week 8332

Month 90

All 5085049

Currently are 136 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments