A Concise Introduction to Coroutines by Dian-Lun Lin

Today, I will start a miniseries about a scheduler for tasks. The starting point of this miniseries is a straightforward scheduler by Dian-Lun Lin that becomes increasingly sophisticated.

I have already written around 15 posts about coroutines. They explain the theory about coroutines and apply this theory in various ways. However, I still fight for an intuitive introduction to a non-trivial coroutine use case. I was, therefore, pretty happy to watch Dian-Lun Lin CppCon 2022 talk: “An Introduction to C++ Coroutines through a Thread Scheduling Demonstration“.

Today, I’m happy to present a guest post by Dian-Lun Lin. He will intuitively introduce coroutines to implement a straightforward scheduler that dispatches tasks. I will use this scheduler as a starting point for further experiments.

An Introduction to C++ Coroutines

A coroutine is a function that can suspend itself and be resumed by the caller. Unlike regular functions, which execute sequentially from start to finish, coroutines allow for controlled suspension and resumption of execution. This enables us to write code that looks synchronous but can efficiently handle asynchronous operations without blocking the calling thread.
Implementing a C++ coroutine can be a bit challenging due to its versatility. In C++ coroutines, you can fine-tune how a coroutine behaves in numerous ways. For example, you can decide whether a coroutine should be suspended when it starts or finishes, and you can precisely adjust when and where these suspensions occur within the coroutine. To illustrate, let’s begin with a straightforward example:

// simpleCoroutine.cpp

#include <coroutine>
#include <iostream>

struct MyCoroutine {                             // (1)
    struct promise_type {
        MyCoroutine get_return_object() {
            return std::coroutine_handle<promise_type>::from_promise(*this);
        }
        std::suspend_always initial_suspend() {
            return {};
        }
        std::suspend_always final_suspend() noexcept {
            return {};
        }
        void return_void() {}
        void unhandled_exception() {}
    };
    MyCoroutine(std::coroutine_handle<promise_type> handle): handle{handle} {}
    
    void resume() { 
        handle.resume(); 
    }
    void destroy() { 
        handle.destroy(); 
    }
    
    std::coroutine_handle<promise_type> handle;
};

MyCoroutine simpleCoroutine() {                      // (2)
    std::cout << "Start coroutine\n";
    co_await std::suspend_always{};
    std::cout << "Resume coroutine\n";
}

int main() {
    MyCoroutine coro = simpleCoroutine();
    std::cout << "Coroutine is not executed yet\n";
    coro.resume();
    std::cout << "Suspend coroutine\n";
    coro.resume();
    coro.destroy();
    return 0;
}


This example code demonstrates the basic usage of C++ coroutines and provides an example of a custom coroutine. To implement a C++ coroutine, you must understand four essential components: Coroutine, Promise type, Awaitable, and Coroutine handle. I will explain each component through the example code in the following sections.

Coroutine

In C++, coroutines are implemented through the co_return, co_await, and co_yield keywords. These keywords allow developers to express asynchronous behavior in a structured and intuitive way. In the example coroutine, simpleCoroutine, I call co_await std::suspend always{} to suspend the coroutine. The std::suspend_always is a C++ standard provided awaitable that always suspends the coroutine.

When you call the function simpleCoroutine, it doesn’t execute the coroutine immediately. Instead, it returns a coroutine object that defines promise type. Line (2) defines the function simpleCoroutine that returns a MyCoroutine object. In line (1), I define the MyCoroutine class and the promise type. You might wonder why calling a coroutine function doesn’t immediately execute it. Again, this is because C++ Coroutine is designed to be flexible. C++ Coroutine allows you to decide when and how a coroutine should begin and end. This is defined in the promise_type.

Promise Type

A promise_type controls a coroutine’s behavior. Here are the key responsibilities of a promise_type:

  • Creating the Coroutine Object: The get_return_object function creates an instance of the coroutine and returns it to the caller.
  • Controlling Suspension: The initial_suspend and final_suspend functions determine whether the coroutine should be suspended or resumed at the beginning and the end. They return awaitables that dictate how the coroutine behaves.
  • Handling Return Values: The return_value function sets the return value of the coroutine when it completes. It enables the coroutine to produce a result that the caller can retrieve. In the example code, I use return_void, indicating this coroutine does not have the return value.
  • Handling Exceptions: The unhandled_exception function is invoked when an unhandled exception occurs within the coroutine. It provides a mechanism to handle exceptions gracefully.

But where is the promise_type used? I do not see the word ”promise” in the example code. Actually, when you write your coroutine, the compiler sees your code slightly differently. The compiler’s simplified view of simpleCoroutine is the following:

MyCoroutine simpleCoroutine() {
    MyCoroutine::promise_type p();
    MyCoroutine coro_obj = p.get_return_object();

    try {
      co_await p.inital_suspend();
      std::cout << "Start coroutine\n";
      co_await std::suspend_always{};
      std::cout << "Resume coroutine\n";
    } catch(...) {
      p.unhandled_exception();
    }
    co_await p.final_suspend();
}

That’s why you need to define promise_type in the MyCoroutine class. When simpleCoroutine is called, the compiler creates a promise_type and calls get_return_object() to create the MyCoroutine object. Before the coroutine body, the compiler calls initial_suspend to determine whether to suspend at the beginning. Finally, it calls final_suspend to determine whether to suspend at the end. You will get compilation errors if you don’t define promise_type and its corresponding functions.

 

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.

     

    Awaitable

    An awaitable controls a suspension point behavior. Three functions need to be defined for an awaitable:

    • await_ready: This function determines whether the coroutine can proceed without suspension. It should return true if the operation is ready to continue immediately, or false if suspension is required. This method is an optimization that allows you to avoid the cost of suspension in cases where it is known that the operation will be completed synchronously.
    • await_suspend: This function provides fine-grained control over a suspension point behavior. It passes the current coroutine handle for users to resume the coroutine later or destroy the coroutine. There are three return types for this function:
      • void: We suspend the coroutine. The control is immediately returned to the caller of the current coroutine.
      • bool: If true, we suspend the current coroutine and return control to the caller; If false, we resume the current coroutine.
      • coroutine_handle: We suspend the current coroutine and resume that returned coroutine handle. This is also called asymmetric transfer.
    • await_resume: This function specifies what value should be returned to the coroutine when the awaited operation is complete. It resumes the coroutine’s execution, passing the awaited result. If no result is expected or needed, this function can be empty and return void.

    But where are these functions used? Again, let’s look into the compiler’s view. When you call co_await std:suspend_always{}, the compiler will transform it into the following code:

    auto&& awaiter = std::suspend_always{};
      if(!awaiter.await_ready()) {
        awaiter.await_suspend(std::coroutine_handle<>...);
        //<suspend/resume>
      }
    awaiter.await_resume();
    

    That’s why you need to define all these functions. The std::suspend_always is a C++ built-in awaitable that defines the functions as follows:

    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 {}
    };
    

    Coroutine Handle

    Coroutine handles are used to manage the state and lifecycle of a coroutine. They provide a way to access, resume, and destroy coroutines explicitly. In the example, I call handle.resume() to resume the coroutine and call handle.destroy() to destroy the coroutine.


    After executing the program, the results are the following:

    What’s Next?

    As promised, this post from Dian-Lun Lin was a concise introduction to coroutines. In the next post, Dian-Lun applies the theory to implement a single-threaded scheduler for C++ coroutines.

    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, 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, moon, Philipp Lenk, Hobsbawm, Charles-Jianye Chen, and Keith Jeffery.

    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,