Namespaces
Variants

Coroutines (C++20)

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

协程是一种能够暂停执行并在之后恢复的函数。协程是无栈的:它们通过返回到调用者来暂停执行,且恢复执行所需的数据与栈分开存储。这使得顺序代码能够异步执行(例如,无需显式回调即可处理非阻塞I/O),同时支持对惰性计算的无限序列进行算法操作及其他用途。

满足以下任一条件的函数即为协程:

  • co_await 表达式 —— 用于暂停执行直至恢复
task<> tcp_echo_server()
{
    char data[1024];
    while (true)
    {
        std::size_t n = co_await socket.async_read_some(buffer(data));
        co_await async_write(socket, buffer(data, n));
    }
}
  • the co_yield 表达式 —— 用于暂停执行并返回一个值
generator<unsigned int> iota(unsigned int n = 0)
{
    while (true)
        co_yield n++;
}
  • co_return 语句 —— 用于完成执行并返回值
lazy<int> f()
{
    co_return 7;
}

每个协程都必须具有满足以下若干要求的返回类型。

目录

限制条件

协程不能使用 可变参数 、普通 返回 语句或 占位符返回类型 auto 概念 )。

Consteval 函数 constexpr 函数 构造函数 析构函数 以及 main 函数 不能作为协程使用。

执行

每个协程关联于

  • promise 对象 ,在协程内部进行操作。协程通过此对象提交其结果或异常。promise 对象与 std::promise 完全无关。
  • coroutine handle ,在协程外部进行操作。这是一个非拥有式句柄,用于恢复协程执行或销毁协程帧。
  • coroutine state ,是内部的动态分配存储(除非分配被优化掉),包含以下内容的对象
  • promise 对象
  • 参数(全部按值复制)
  • 当前挂起点的某种表示形式,以便 resume 操作知道从何处继续执行,destroy 操作知道哪些局部变量在作用域内
  • 生命周期跨越当前挂起点的局部变量和临时变量

当协程开始执行时,它会执行以下操作:

  • 使用 operator new 分配 协程状态对象。
  • 将所有函数形参复制到协程状态:按值传递的形参将被移动或复制,按引用传递的形参保持引用关系(因此,如果在引用对象生命周期结束后恢复协程,可能导致悬空引用——详见后续示例)。
  • 调用 promise 对象的构造函数。如果 promise 类型具有接收所有协程参数的构造函数,则使用复制后的协程实参调用该构造函数;否则调用默认构造函数。
  • 调用 promise. get_return_object ( ) 并将结果保存在局部变量中。该调用的结果将在协程首次暂停时返回给调用方。在此步骤之前(含)抛出的任何异常都会传播回调用方,不会存入 promise 对象。
  • 调用 promise. initial_suspend ( ) 并对结果执行 co_await 。典型的 Promise 类型要么返回 std::suspend_always (用于惰性启动的协程),要么返回 std::suspend_never (用于急切启动的协程)。
  • co_await promise. initial_suspend ( ) 恢复执行时,开始执行协程函数体。

参数变为悬垂的一些示例:

#include <coroutine>
#include <iostream>
struct promise;
struct coroutine : std::coroutine_handle<promise>
{
    using promise_type = ::promise;
};
struct promise
{
    coroutine get_return_object() { return {coroutine::from_promise(*this)}; }
    std::suspend_always initial_suspend() noexcept { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }
    void return_void() {}
    void unhandled_exception() {}
};
struct S
{
    int i;
    coroutine f()
    {
        std::cout << i;
        co_return;
    }
};
void bad1()
{
    coroutine h = S{0}.f();
    // S{0} 已被销毁
    h.resume(); // 恢复的协程执行 std::cout << i,在释放后使用 S::i
    h.destroy();
}
coroutine bad2()
{
    S s{0};
    return s.f(); // 返回的协程在恢复时无法避免释放后使用
}
void bad3()
{
    coroutine h = [i = 0]() -> coroutine // 同时也是协程的 lambda 表达式
    {
        std::cout << i;
        co_return;
    }(); // 立即调用
    // lambda 已销毁
    h.resume(); // 在释放后使用(匿名 lambda 类型)::i
    h.destroy();
}
void good()
{
    coroutine h = [](int i) -> coroutine // 将 i 作为协程参数
    {
        std::cout << i;
        co_return;
    }(0);
    // lambda 已销毁
    h.resume(); // 没有问题,i 已作为按值参数复制到协程帧中
                // frame as a by-value parameter
    h.destroy();
}

当协程到达挂起点

  • 先前获取的返回对象将返回给调用者/恢复者,如有必要,会隐式转换为协程的返回类型。

当协程执行到 co_return 语句时,将执行以下操作:

  • promise. return_void ( ) 进行调用
  • co_return ;
  • co_return expr ; 其中 expr 具有 void 类型
  • 对于 co_return expr ; (其中 expr 具有非 void 类型),调用 promise. return_value ( expr )
  • 按创建顺序的逆序销毁所有具有自动存储期的变量
  • 调用 promise. final_suspend ( ) 并对结果执行 co_await

从协程末尾退出等同于 co_return ; ,区别在于:如果在 Promise 作用域内找不到任何 return_void 声明,则其行为是未定义的。若函数体中不包含任何定义性关键字,无论其返回类型如何,该函数都不是协程;并且当返回类型不是(可能带有 cv 限定符的) void 时,从末尾退出将导致未定义行为。

// 假设 task 是某种协程任务类型
task<void> f()
{
    // 不是协程,未定义行为
}
task<void> g()
{
    co_return;  // 正确
}
task<void> h()
{
    co_await g();
    // 正确,隐式 co_return;
}

如果协程以未捕获的异常结束,它将执行以下操作:

  • 捕获异常并在 catch 块内调用 promise. unhandled_exception ( )
  • 调用 promise. final_suspend ( ) 并对结果执行 co_await (例如恢复延续任务或发布结果)。从此处恢复协程将导致未定义行为。

当协程状态因通过 co_return 或未捕获异常而终止,或因通过其句柄被销毁而销毁时,它会执行以下操作:

  • 调用 promise 对象的析构函数。
  • 调用函数参数副本的析构函数。
  • 调用 operator delete 释放协程状态所占用的内存。
  • 将执行权交还给调用者/恢复者。

动态分配

协程状态通过非数组形式的 operator new 进行动态分配。

如果 Promise 类型定义了类级别的替换,则将使用该替换,否则将使用全局的 operator new

如果 Promise 类型定义了接受额外参数的 operator new 放置形式,且这些参数与参数列表匹配(其中第一个参数是请求的大小(类型为 std::size_t ),其余参数是协程函数参数),那么这些参数将被传递给 operator new (这使得对协程使用 前导分配器约定 成为可能)。

operator new 的调用可以被优化掉(即使使用了自定义分配器),如果

  • 协程状态的生命周期严格嵌套在调用者的生命周期内,且
  • 协程帧的大小在调用点已知。

在这种情况下,协程状态会嵌入到调用者的栈帧中(如果调用者是普通函数)或协程状态中(如果调用者是协程)。

如果分配失败,协程将抛出 std::bad_alloc ,除非 Promise 类型定义了成员函数 Promise :: get_return_object_on_allocation_failure ( ) 。若定义了该成员函数,分配将使用 operator new 的无异常抛出形式,并在分配失败时,协程立即将从 Promise :: get_return_object_on_allocation_failure ( ) 获取的对象返回给调用方,例如:

struct Coroutine::promise_type
{
    /* ... */
    // 确保使用不抛出异常的 operator-new
    static Coroutine get_return_object_on_allocation_failure()
    {
        std::cerr << __func__ << '\n';
        throw std::bad_alloc(); // 或者返回 Coroutine(nullptr);
    }
    // 自定义不抛出异常的 new 重载
    void* operator new(std::size_t n) noexcept
    {
        if (void* mem = std::malloc(n))
            return mem;
        return nullptr; // 分配失败
    }
};

Promise

Promise 类型由编译器通过 std::coroutine_traits 从协程的返回类型推导确定。

形式上,令

  • R Args... 分别表示协程的返回类型和参数类型列表,
  • ClassT 表示协程所属的类类型(若其定义为非静态成员函数),
  • cv 表示在 函数声明 中声明的 cv 限定符(若其定义为非静态成员函数),

Promise 类型由以下因素决定:

例如:

若协程定义为... 则其 Promise 类型为...
task < void > foo ( int x ) ; std:: coroutine_traits < task < void > , int > :: promise_type
task < void > Bar :: foo ( int x ) const ; std:: coroutine_traits < task < void > , const Bar & , int > :: promise_type
task < void > Bar :: foo ( int x ) && ; std:: coroutine_traits < task < void > , Bar && , int > :: promise_type

co_await

一元运算符 co_await 会暂停协程的执行并将控制权返还给调用者。

co_await 表达式

co_await 表达式仅可出现在常规 函数体 (包括 lambda 表达式 的函数体)内的 潜在求值表达式 中,且不得出现于

co_await 表达式不能作为 潜在求值子表达式 出现在 契约断言 的谓词中。

(since C++26)

首先, expr 按如下方式转换为可等待对象:

  • 如果 expr 由初始暂停点、最终暂停点或 yield 表达式产生,则可等待对象保持 expr 原样。
  • 否则,若当前协程的 Promise 类型具有成员函数 await_transform ,则可等待对象为 promise. await_transform ( expr )
  • 否则,可等待对象保持 expr 原样。

随后,获取等待器对象,如下所示:

  • 如果对 operator co_await 的重载解析给出了唯一最佳重载,则等待器是该调用的结果:
  • awaitable. operator co_await ( ) 用于成员重载,
  • operator co_await ( static_cast < Awaitable && > ( awaitable ) ) 用于非成员重载。
  • 否则,若重载决议找不到运算符 co_await ,则等待器(awaiter)本身可作为可等待对象(awaitable)。
  • 否则,若重载决议存在歧义,则程序非良构。

若上述表达式为 纯右值 ,则等待器对象是由其 实质化 生成的临时对象。否则,若上述表达式为 泛左值 ,则等待器对象即其所指代的对象。

然后,调用 awaiter. await_ready ( ) (这是一个快捷方式,用于在已知结果已就绪或可同步完成时避免挂起开销)。如果其结果在上下文转换为 bool 后为 false ,则

协程被暂停(其协程状态中填充了局部变量和当前暂停点)。
awaiter. await_suspend ( handle ) 被调用,其中 handle 是代表当前协程的协程句柄。在该函数内部,被暂停的协程状态可通过该句柄被观察,且该函数负责将其调度到某个执行器上恢复运行或安排其销毁(返回 false 视为调度操作)
  • await_suspend 返回 void ,控制权立即返回给当前协程的调用者/恢复者(此协程保持暂停状态),否则
  • await_suspend 返回 bool
  • true 将控制权返回给当前协程的调用者/恢复者
  • false 恢复当前协程
  • await_suspend 返回其他协程的协程句柄,则该句柄被恢复(通过调用 handle. resume ( ) )(注意这可能链式触发最终导致当前协程恢复)
  • await_suspend 抛出异常,异常会被捕获,协程被立即恢复,并重新抛出该异常

最后,调用 awaiter. await_resume ( ) (无论协程是否被挂起),其返回值即为整个 co_await expr 表达式的结果。

如果协程在 co_await 表达式中被挂起,之后又被恢复执行,恢复点将位于调用 awaiter. await_resume ( ) 之前。

请注意,协程在进入 awaiter. await_suspend ( ) 之前已完全挂起。其句柄可被共享至其他线程,并在 await_suspend ( ) 函数返回前恢复执行。(请注意默认内存安全规则仍然适用,若协程句柄未经锁保护跨线程共享,则等待器应至少使用 释放语义 ,而恢复方应至少使用 获取语义 。)例如,可将协程句柄置入回调函数,在异步I/O操作完成时调度至线程池运行。在此场景下,由于当前协程可能已被恢复并执行了等待器对象的析构函数,这些操作与 await_suspend ( ) 在当前线程上继续执行的过程并发发生,因此 await_suspend ( ) 应将 * this 视为已销毁对象,在句柄发布至其他线程后不得再访问其成员。

示例

#include <coroutine>
#include <iostream>
#include <stdexcept>
#include <thread>
auto switch_to_new_thread(std::jthread& out)
{
    struct awaitable
    {
        std::jthread* p_out;
        bool await_ready() { return false; }
        void await_suspend(std::coroutine_handle<> h)
        {
            std::jthread& out = *p_out;
            if (out.joinable())
                throw std::runtime_error("Output jthread parameter not empty");
            out = std::jthread([h] { h.resume(); });
            // 潜在未定义行为:访问可能已被销毁的 *this
            // std::cout << "New thread ID: " << p_out->get_id() << '\n';
            std::cout << "New thread ID: " << out.get_id() << '\n'; // 这样是安全的
        }
        void await_resume() {}
    };
    return awaitable{&out};
}
struct task
{
    struct promise_type
    {
        task get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() {}
    };
};
task resuming_on_new_thread(std::jthread& out)
{
    std::cout << "Coroutine started on thread: " << std::this_thread::get_id() << '\n';
    co_await switch_to_new_thread(out);
    // 等待器在此处被销毁
    std::cout << "Coroutine resumed on thread: " << std::this_thread::get_id() << '\n';
}
int main()
{
    std::jthread out;
    resuming_on_new_thread(out);
}

可能的输出:

Coroutine started on thread: 139972277602112
New thread ID: 139972267284224
Coroutine resumed on thread: 139972267284224

注意:等待器对象是协程状态的一部分(作为跨越挂起点的临时对象),它会在 co_await 表达式完成前被销毁。通过它可以维护某些异步I/O API所需的每次操作状态,而无需借助额外的动态内存分配。

标准库定义了两个基础可等待对象: std::suspend_always std::suspend_never

promise_type :: await_transform 与程序提供的等待器演示

示例

#include <cassert>
#include <coroutine>
#include <iostream>
struct tunable_coro
{
    // 通过构造函数参数确定"就绪状态"的等待器
    class tunable_awaiter
    {
        bool ready_;
    public:
        explicit(false) tunable_awaiter(bool ready) : ready_{ready} {}
        // 三个标准等待器接口函数:
        bool await_ready() const noexcept { return ready_; }
        static void await_suspend(std::coroutine_handle<>) noexcept {}
        static void await_resume() noexcept {}
    };
    struct promise_type
    {
        using coro_handle = std::coroutine_handle<promise_type>;
        auto get_return_object() { return coro_handle::from_promise(*this); }
        static auto initial_suspend() { return std::suspend_always(); }
        static auto final_suspend() noexcept { return std::suspend_always(); }
        static void return_void() {}
        static void unhandled_exception() { std::terminate(); }
        // 用户提供的转换函数,返回自定义等待器:
        auto await_transform(std::suspend_always) { return tunable_awaiter(!ready_); }
        void disable_suspension() { ready_ = false; }
    private:
        bool ready_{true};
    };
    tunable_coro(promise_type::coro_handle h) : handle_(h) { assert(h); }
    // 为简化起见,将这4个特殊函数声明为已删除:
    tunable_coro(tunable_coro const&) = delete;
    tunable_coro(tunable_coro&&) = delete;
    tunable_coro& operator=(tunable_coro const&) = delete;
    tunable_coro& operator=(tunable_coro&&) = delete;
    ~tunable_coro()
    {
        if (handle_)
            handle_.destroy();
    }
    void disable_suspension() const
    {
        if (handle_.done())
            return;
        handle_.promise().disable_suspension();
        handle_();
    }
    bool operator()()
    {
        if (!handle_.done())
            handle_();
        return !handle_.done();
    }
private:
    promise_type::coro_handle handle_;
};
tunable_coro generate(int n)
{
    for (int i{}; i != n; ++i)
    {
        std::cout << i << ' ';
        // 传递给 co_await 的等待器会进入 promise_type::await_transform,
        // 该函数返回 tunable_awaiter,初始会导致挂起(每次迭代时返回到 main),
        // 但在调用 disable_suspension 后不再发生挂起,
        // 循环将运行至结束而不返回 main()。
        co_await std::suspend_always{};
    }
}
int main()
{
    auto coro = generate(8);
    coro(); // 仅输出第一个元素 == 0
    for (int k{}; k < 4; ++k)
    {
        coro(); // 输出 1 2 3 4,每次迭代一个
        std::<span class="me

co_yield

co_yield 表达式向调用者返回一个值并暂停当前协程:它是可恢复生成器函数的基础构建模块。

co_yield 表达式
co_yield 花括号初始化列表

它等价于

co_await promise.yield_value(expr)

典型的生成器 yield_value 会将其参数存储(复制/移动或仅存储地址,因为参数的生命周期跨越了 co_await 内部的暂停点)到生成器对象中,并返回 std::suspend_always ,从而将控制权转移给调用者/恢复者。

#include <coroutine>
#include <cstdint>
#include <exception>
#include <iostream>
template<typename T>
struct Generator
{
    // 类名'Generator'是我们的选择,协程不需要它
    // 编译器通过'co_yield'关键字识别协程
    // 只要包含嵌套结构体promise_type及其'MyGenerator get_return_object()'方法
    // 你可以使用'MyGenerator'(或任何其他名称)代替
    //(注意:重命名时需要调整构造函数和析构函数的声明)
    struct promise_type;
    using handle_type = std::coroutine_handle<promise_type>;
    struct promise_type // 必需
    {
        T value_;
        std::exception_ptr exception_;
        Generator get_return_object()
        {
            return Generator(handle_type::from_promise(*this));
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void unhandled_exception() { exception_ = std::current_exception(); } // 保存
                                                                              // 异常
        template<std::convertible_to<T> From> // C++20 概念
        std::suspend_always yield_value(From&& from)
        {
            value_ = std::forward<From>(from); // 在promise中缓存结果
            return {};
        }
        void return_void() {}
    };
    handle_type h_;
    Generator(handle_type h) : h_(h) {}
    ~Generator() { h_.destroy(); }
    explicit operator bool()
    {
        fill(); // 可靠地判断协程是否完成,以及是否会有下一个生成值(co_yield)
                // 的唯一方法是通过C++获取器(下面的operator())执行/恢复协程,
                // 直到下一个co_yield点(或让其自然结束)。
                // 然后我们在promise中存储/缓存结果,允许获取器(下面的operator())
                // 在不执行协程的情况下获取它。
        return !h_.done();
    }
    T operator()()
    {
        fill();
        full_ = false; // 我们将移出先前缓存的结果,
                       // 使promise再次为空
        return std::move(h_.promise().value_);
    }
private:
    bool full_ = false;
    void fill()
    {
        if (!full_)
        {
            h_();
            if (h_.promise().exception_)
                std::rethrow_exception(h_.promise().exception_);
            // 在调用上下文中传播协程异常
            full_ = true;
        }
    }
};
Generator<std::uint64_t>
fibonacci_sequence(unsigned n)
{
    if (n == 0)
        co_return;
    if (n > 94)
        throw std::runtime_error("Too big Fibonacci sequence. Elements would overflow.");
    co_yield 0;
    if (n == 1)
        co_return;
    co_yield 1;
    if (n == 2)
        co_return;
    std::uint64_t a = 0;
    std::uint64_t b = 1;
    for (unsigned i = 2; i < n; ++i)
    {
        std::uint64_t s = a + b;
        co_yield s;
        a = b;
        b = s;
    }
}
int main()
{
    try
    {
        auto gen = fibonacci_sequence(10); // uint64_t溢出前最大94
        for (int j = 0; gen; ++j)
            std::cout << "fib(" << j << ")=" << gen() << '\n';
   

注释

功能测试 标准 功能特性
__cpp_impl_coroutine 201902L (C++20) 协程 (编译器支持)
__cpp_lib_coroutine 201902L (C++20) 协程 (库支持)
__cpp_lib_generator 202207L (C++23) std::generator : 用于范围的同步协程生成器

关键词

co_await , co_return , co_yield

库支持

协程支持库 定义了若干类型,为协程提供编译时和运行时支持。

缺陷报告

以下行为变更缺陷报告被追溯应用于先前发布的C++标准。

缺陷报告 应用于 发布时的行为 正确行为
CWG 2556 C++20 无效的 return_void 导致从协程末尾退出的行为未定义 此情况下程序格式错误
CWG 2668 C++20 co_await 不能出现在 lambda 表达式中 允许使用
CWG 2754 C++23 为显式对象成员函数构造 promise 对象时获取了 * this 此情况下不获取 * this

参见

(C++23)
表示同步 协程 生成器的 view
(类模板)

外部链接

1. Lewis Baker, 2017-2022 - 非对称传输。
2. David Mazières, 2021 - C++20协程教程。
3. 徐传奇 & 戚煜 & 韩垚, 2021 - C++20协程原理与应用。(中文)
4. Simon Tatham, 2023 - 编写自定义C++20协程系统。