Deep Dive into C++ Coroutines

happyer - Mar 10 - - Dev Community

1. Preface

Coroutines are a programming paradigm that allows the execution of functions to be suspended and resumed, rather than running to completion once started, as with traditional functions. Unlike threads, which are scheduled by the operating system kernel, coroutines are scheduled in user space, giving them an advantage in lightweight task switching.

The C++20 standard introduced native support for coroutines, implemented through a series of new keywords and library support. Below, we will detail the workings and usage of C++20 coroutines.

2. C++20 New Features

The C++20 standard introduced many new language features, library components, and improvements aimed at enhancing the expressiveness, performance, safety, and usability of C++. Here are some notable new components and libraries in C++20:

2.1. Language Features

  1. Concepts: Introduced constraints for templates, making template programming clearer and easier to understand.
  2. Coroutines: Provided a new programming paradigm for writing asynchronous and non-blocking code.
  3. Spaceship Operator: Introduced the <=> operator for simplifying class comparison operations.
  4. Modules: Provided a new compilation unit to replace traditional header files, improving compile times and encapsulation.
  5. Range-based for loop enhancements: Allowed the use of initialization statements.
  6. Aggregate initialization enhancements: Allowed the use of direct list initialization syntax.
  7. Constexpr improvements: Expanded the use of constexpr to include virtual functions, try-catch blocks, and more.
  8. Designated initializers: Allowed the use of C-style designated initializers.

2.2. Standard Library Components

  1. Ranges library: Introduced the concepts of ranges and views, providing a new way to manipulate sequences.
  2. span: Provided a lightweight, non-owning type representing an array view.
  3. bit: Provided a toolkit for bit manipulation, including bit counting, rotation, and more.
  4. syncstream: Introduced synchronized stream operations for I/O in a multi-threaded environment.
  5. format: Provided a type-safe string formatting library, similar to Python's str.format or C#'s string interpolation.
  6. std::jthread: Introduced a new thread class that automatically joins its thread.
  7. std::latch and std::barrier: Provided new synchronization primitives.
  8. std::atomic_ref: Provided atomic operations for non-atomic types.
  9. std::stop_token and std::stop_source: Used to control stop requests for threads.
  10. std::counted_ptr: A reference-counted smart pointer for managing shared resources.

2.3. Improvements and Extensions

  1. constexpr support for standard library algorithms: Many standard library algorithms can now be used at compile time.
  2. Improvements to the PImpl idiom: Simplified the use of the PImpl (Pointer to Implementation) idiom by supporting incomplete types for unique_ptr and shared_ptr.
  3. Improvements to the time library: Added support for time zones and calendars.
  4. std::source_location: Provided a way to obtain the current source code location for debugging and logging.
  5. std::to_address: Used to obtain the address pointed to by a pointer or smart pointer.

These new features and library components in C++20 provide C++ programmers with more tools and capabilities, making writing modern C++ code more efficient and enjoyable. As the C++ community continues to evolve, we can expect future C++ standards to introduce more improvements and new features.

3. Basic Concepts of C++20 Coroutines

C++20 coroutines are implemented by introducing several new keywords: co_return, co_await, and co_yield. These keywords define different behaviors of coroutines:

  • co_return is used to specify the return value of a coroutine.
  • co_await is used to suspend a coroutine until the awaited condition is satisfied.
  • co_yield is used to produce a value to the caller of the coroutine and temporarily suspend the coroutine.

4. Structure and Key Components of Coroutines

The implementation of coroutines relies on several core components:

  1. Coroutine handle: This is a pointer to the coroutine state, which can be used to resume or destroy the coroutine.

  2. Promise type of the coroutine: This is a user-defined type that defines the return object of the coroutine, the logic for suspension and resumption, and how to handle return values and exceptions.

  3. Awaiter: This is an object that defines the behavior of a coroutine when it encounters co_await. The awaiter must implement three methods: await_ready, await_suspend, and await_resume.

5. Lifecycle of a Coroutine

The lifecycle of a coroutine can be divided into several stages:

  1. Creation: When a coroutine function is called, a coroutine frame is created, which is a data structure containing the coroutine's state.

  2. Initial Suspension: The coroutine begins execution, but before running any actual code, it asks the promise type whether to suspend immediately.

  3. Execution: The coroutine starts executing user-defined code until it encounters co_await, co_yield, or co_return.

  4. Suspension: When the coroutine encounters co_await or co_yield, it suspends according to the logic of the awaiter.

  5. Resumption: The coroutine can be resumed at some later point in time, continuing execution until the next suspension point or completion.

  6. Final Suspension: After the coroutine has finished executing, it asks the promise type whether to suspend again.

  7. Destruction: Finally, the coroutine frame is destroyed, releasing all resources.

6. Example Code Analysis

Below is a simple C++20 coroutine example that demonstrates the basic usage of coroutines:

#include <coroutine>
#include <iostream>
#include <string>

// Coroutine promise type
struct promise_task {
    // ... definition of promise_type ...
};

// Awaiter
struct awaitable {
    // ... definition of awaitable ...
};

// Coroutine function
promise_task test(std::string str) {
    int ct = 0;
    ct++;
    std::cout << "test start--------- " << '\n';
    co_await awaitable{};
    co_yield 201;
    co_return str;
}

int main() {
    std::cout << "main start--------- " << '\n';
    promise_task task = test("test input");
    std::cout << "main end--------- " << '\n';
    // ... using the coroutine ...
}
Enter fullscreen mode Exit fullscreen mode

In this example, the test function is a coroutine function that uses co_await to suspend the coroutine, co_yield to produce a value, and co_return to return a string. promise_task and awaitable are user-defined types that implement the interfaces required by the coroutine promise type and awaiter, respectively.

7. Implementing Suspension and Resumption

Implementing suspension and resumption is one of the core features of C++20 coroutines. When a coroutine encounters a co_await expression during execution, it may be suspended. The suspended coroutine relinquishes control back to the caller until it is triggered to resume execution by an external event (such as I/O operation completion, timer expiration, signals from other coroutines, etc.). Resuming a coroutine is typically done by calling the resume method on the coroutine handle. Below, we will detail this process through an example.

7.1. Example Code

Suppose we have an asynchronous task that needs to wait for some time before it can continue execution. We can use std::chrono and std::this_thread to simulate this asynchronous waiting process.

First, we define an awaiter simple_awaiter that will resume the coroutine after a period of time:

#include <coroutine>
#include <iostream>
#include <thread>
#include <chrono>

// Awaiter
struct simple_awaiter {
    std::chrono::milliseconds delay;

    bool await_ready() const noexcept {
        return delay.count() <= 0; // If there is no delay, execute immediately
    }

    void await_suspend(std::coroutine_handle<> handle) const {
        std::thread([handle, this] {
            std::this_thread::sleep_for(delay); // Simulate asynchronous waiting
            handle.resume(); // Resume the coroutine after the delay
        }).detach(); // Start a new thread and detach it
    }

    void await_resume() const noexcept {
        // No operation, continue execution after resuming
    }
};
Enter fullscreen mode Exit fullscreen mode

Next, we define a coroutine function async_wait that uses simple_awaiter to suspend and resume:

struct task {
    struct promise_type {
        task get_return_object() {
            return {std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() {}
    };

    std::coroutine_handle<promise_type> handle_;
};

// Coroutine function
task async_wait(std::chrono::milliseconds delay) {
    std::cout << "Coroutine is about to be suspended..." << std::endl;
    co_await simple_awaiter{delay}; // Suspend the coroutine
    std::cout << "Coroutine has been resumed!" << std::endl;
    co_return; // End the coroutine
}
Enter fullscreen mode Exit fullscreen mode

Finally, in the main function, we create and start the coroutine:

int main() {
    std::cout << "Main start" << std::endl;
    auto my_task = async_wait(std::chrono::seconds(2)); // Start the coroutine and return immediately
    std::cout << "Main continues while coroutine is suspended" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(3)); // Wait enough time to ensure the coroutine has resumed
    std::cout << "Main end" << std::endl;
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

7.2. Output

Main start
Coroutine is about to be suspended...
Main continues while coroutine is suspended
Coroutine has been resumed!
Main end
Enter fullscreen mode Exit fullscreen mode

7.3. Analysis

In this example, the async_wait function is a coroutine that is suspended when it executes co_await simple_awaiter{delay};. The simple_awaiter's await_suspend method starts a new thread and waits for the specified time in that thread. After the wait is complete, it calls handle.resume() to resume the execution of the coroutine.

While the coroutine is suspended, control returns to the main function, allowing the program to continue executing other tasks. In our example, the main function simply waits for 3 seconds to ensure that the coroutine has enough time to resume and complete execution.

It is worth noting that the thread in simple_awaiter is detached (by calling detach()), meaning we do not need to explicitly wait for it to complete in the main function. However, in a real application, you might need more fine-grained control to manage the lifecycle and resource cleanup of threads.

This example demonstrates the basic mechanism of coroutine suspension and resumption. In practice, coroutines are often used for handling I/O operations, network requests, concurrent tasks, and other asynchronous programming scenarios. The design of C++20 coroutines provides a more intuitive and manageable way to write such code.

8. Developing any platform from Scratch with Codia AI Code

To integrate Codia AI into your Figma to any platform such as frontend, mobile, and Mac development process, follow these instructions:
Open the link: Codia AI Figma to code: HTML, CSS, React, Vue, iOS, Android, Flutter, ReactNative, Tailwind, Web, App

Open the link

  • Install the Codia AI Plugin: Search for and install the Codia AI Figma to Flutter plugin from the Figma plugin store.
  • Prepare Your Figma Design: Arrange your Figma design with clearly named layers and components to ensure the best code generation results.
  • Convert with Codia AI: Select your design or component in Figma and use Codia AI to instantly

Install the Codia AI Plugin

generate any platform code.

generate any platform code

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .