命名空间
变体
操作

协程 (C++20)

来自 cppreference.cn
< cpp‎ | language
 
 
C++ 语言
 
 

协程是一种可以暂停执行并在之后恢复的函数。协程是无栈的:它们通过返回到调用者来暂停执行,并且恢复执行所需的数据与堆栈分开存储。这允许顺序代码异步执行(例如,处理非阻塞 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));
    }
}
  • co_yield 表达式 — 暂停执行并返回值
generator<unsigned int> iota(unsigned int n = 0)
{
    while (true)
        co_yield n++;
}
  • co_return 语句 — 完成执行并返回值
lazy<int> f()
{
    co_return 7;
}

每个协程都必须有一个满足若干要求的返回类型,如下所述。

目录

[编辑] 限制

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

Consteval 函数constexpr 函数构造函数析构函数main 函数不能是协程。

[编辑] 执行

每个协程都与以下内容关联

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

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

  • 分配协程状态对象,使用 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} destroyed
    h.resume(); // resumed coroutine executes std::cout << i, uses S::i after free
    h.destroy();
}
 
coroutine bad2()
{
    S s{0};
    return s.f(); // returned coroutine can't be resumed without committing use after free
}
 
void bad3()
{
    coroutine h = [i = 0]() -> coroutine // a lambda that's also a coroutine
    {
        std::cout << i;
        co_return;
    }(); // immediately invoked
    // lambda destroyed
    h.resume(); // uses (anonymous lambda type)::i after free
    h.destroy();
}
 
void good()
{
    coroutine h = [](int i) -> coroutine // make i a coroutine parameter
    {
        std::cout << i;
        co_return;
    }(0);
    // lambda destroyed
    h.resume(); // no problem, i has been copied to the coroutine
                // frame as a by-value parameter
    h.destroy();
}

当协程到达暂停点时

  • 先前获得的返回对象在必要时隐式转换为协程的返回类型后,返回给调用者/恢复者。

当协程到达 co_return 语句时,它执行以下操作

  • 调用 promise.return_void() 用于
  • co_return;
  • co_return expr; 其中 expr 的类型为 void
  • 或调用 promise.return_value(expr) 用于 co_return expr; 其中 expr 的类型为非 void
  • 按照创建的相反顺序销毁所有具有自动存储期的变量。
  • 调用 promise.final_suspend()co_await 结果。

协程执行到末尾等同于 co_return;,但如果在 Promise 的作用域中找不到 return_void 的声明,则行为未定义。函数体中没有定义关键字的函数不是协程,无论其返回类型如何,如果返回类型不是(可能是 cv 限定的)void,则执行到末尾会导致未定义的行为。

// assuming that task is some coroutine task type
task<void> f()
{
    // not a coroutine, undefined behavior
}
 
task<void> g()
{
    co_return;  // OK
}
 
task<void> h()
{
    co_await g();
    // OK, implicit co_return;
}

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

  • 捕获异常并在 catch 块内调用 promise.unhandled_exception()
  • 调用 promise.final_suspend()co_await 结果(例如,恢复 continuation 或发布结果)。从这一点恢复协程是未定义的行为。

当协程状态被销毁时,无论是由于通过 co_return 或未捕获的异常终止,还是由于通过其句柄销毁,它都会执行以下操作

  • 调用 promise 对象的析构函数。
  • 调用函数参数副本的析构函数。
  • 调用 operator delete 来释放协程状态使用的内存。
  • 将执行权转移回调用者/恢复者。

[编辑] 动态分配

协程状态通过非数组 operator new 动态分配。

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

如果 Promise 类型定义了 placement 形式的 operator new,它接受额外的参数,并且这些参数与参数列表匹配,其中第一个参数是请求的大小(类型为 std::size_t),其余参数是协程函数参数,则这些参数将传递给 operator new(这使得可以使用 leading-allocator-convention 用于协程)。

如果满足以下条件,则可以优化掉对 operator new 的调用(即使使用了自定义分配器)

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

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

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

struct Coroutine::promise_type
{
    /* ... */
 
    // ensure the use of non-throwing operator-new
    static Coroutine get_return_object_on_allocation_failure()
    {
        std::cerr << __func__ << '\n';
        throw std::bad_alloc(); // or, return Coroutine(nullptr);
    }
 
    // custom non-throwing overload of new
    void* operator new(std::size_t n) noexcept
    {
        if (void* mem = std::malloc(n))
            return mem;
        return nullptr; // allocation failure
    }
};

[编辑] Promise

Promise 类型由编译器使用 std::coroutine_traits 从协程的返回类型确定。

正式地,设

  • RArgs... 分别表示协程的返回类型和参数类型列表,
  • 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 expr

co_await 表达式只能出现在常规函数体(包括 lambda 表达式的函数体)内的潜在求值表达式中,并且不能出现在

  • handler 中,
  • 声明语句中,除非它出现在该声明语句的初始化器中,
  • init-statement简单声明中(参见 ifswitchfor 和 [[../range-for|range-for]]),除非它出现在该 init-statement 的初始化器中 ,
  • 默认参数中,或
  • 在具有静态或线程存储期的块作用域变量的初始化器中。

co_await 表达式不能是潜在求值契约断言谓词的子表达式。

(自 C++26 起)

首先,expr 按如下方式转换为 awaitable

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

然后,按如下方式获取 awaiter 对象

  • 如果运算符 co_await 的重载决议给出了单个最佳重载,则 awaiter 是该调用的结果
  • awaitable.operator co_await() 用于成员重载,
  • operator co_await(static_cast<Awaitable&&>(awaitable)) 用于非成员重载。
  • 否则,如果重载决议找不到运算符 co_await,则 awaiter 是 awaitable,保持原样。
  • 否则,如果重载决议不明确,则程序格式错误。

如果上面的表达式是 prvalue,则 awaiter 对象是从它具体化的临时对象。否则,如果上面的表达式是 glvalue,则 awaiter 对象是它引用的对象。

然后,调用 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() 函数返回之前恢复。(请注意,默认的内存安全规则仍然适用,因此如果协程句柄在没有锁的情况下跨线程共享,则 awaiter 应至少使用 release 语义,而恢复者应至少使用 acquire 语义。)例如,协程句柄可以放在回调中,计划在异步 I/O 操作完成时在线程池上运行。在这种情况下,由于当前协程可能已被恢复,因此执行了 awaiter 对象的析构函数,所有这些都与 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(); });
            // Potential undefined behavior: accessing potentially destroyed *this
            // std::cout << "New thread ID: " << p_out->get_id() << '\n';
            std::cout << "New thread ID: " << out.get_id() << '\n'; // this is OK
        }
        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);
    // awaiter destroyed here
    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

注意:awaiter 对象是协程状态的一部分(作为生命周期跨越暂停点的临时对象),并在 co_await 表达式完成之前被销毁。它可以用于维护某些异步 I/O API 所需的按操作状态,而无需额外的动态分配。

标准库定义了两个简单的 awaitable:std::suspend_alwaysstd::suspend_never

promise_type::await_transform 和程序提供的 awaiter 演示

[编辑] 示例

#include <cassert>
#include <coroutine>
#include <iostream>
 
struct tunable_coro
{
    // An awaiter whose "readiness" is determined via constructor's parameter.
    class tunable_awaiter
    {
        bool ready_;
    public:
        explicit(false) tunable_awaiter(bool ready) : ready_{ready} {}
        // Three standard awaiter interface functions:
        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(); }
        // A user provided transforming function which returns the custom awaiter:
        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); }
 
    // For simplicity, declare these 4 special functions as deleted:
    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 << ' ';
        // The awaiter passed to co_await goes to promise_type::await_transform which
        // issues tunable_awaiter that initially causes suspension (returning back to
        // main at each iteration), but after a call to disable_suspension no suspension
        // happens and the loop runs to its end without returning to main().
        co_await std::suspend_always{};
    }
}
 
int main()
{
    auto coro = generate(8);
    coro(); // emits only one first element == 0
    for (int k{}; k < 4; ++k)
    {
        coro(); // emits 1 2 3 4, one per each iteration
        std::cout << ": ";
    }
    coro.disable_suspension();
    coro(); // emits the tail numbers 5 6 7 all at ones
}

输出

0 1 : 2 : 3 : 4 : 5 6 7

[编辑] co_yield

co_yield 表达式向调用者返回值并挂起当前协程:它是可恢复生成器函数的常用构建块。

co_yield expr
co_yield braced-init-list

它等价于

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
{
    // The class name 'Generator' is our choice and it is not required for coroutine
    // magic. Compiler recognizes coroutine by the presence of 'co_yield' keyword.
    // You can use name 'MyGenerator' (or any other name) instead as long as you include
    // nested struct promise_type with 'MyGenerator get_return_object()' method.
    // (Note: It is necessary to adjust the declarations of constructors and destructors
    //  when renaming.)
 
    struct promise_type;
    using handle_type = std::coroutine_handle<promise_type>;
 
    struct promise_type // required
    {
        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(); } // saving
                                                                              // exception
 
        template<std::convertible_to<T> From> // C++20 concept
        std::suspend_always yield_value(From&& from)
        {
            value_ = std::forward<From>(from); // caching the result in promise
            return {};
        }
        void return_void() {}
    };
 
    handle_type h_;
 
    Generator(handle_type h) : h_(h) {}
    ~Generator() { h_.destroy(); }
    explicit operator bool()
    {
        fill(); // The only way to reliably find out whether or not we finished coroutine,
                // whether or not there is going to be a next value generated (co_yield)
                // in coroutine via C++ getter (operator () below) is to execute/resume
                // coroutine until the next co_yield point (or let it fall off end).
                // Then we store/cache result in promise to allow getter (operator() below
                // to grab it without executing coroutine).
        return !h_.done();
    }
    T operator()()
    {
        fill();
        full_ = false; // we are going to move out previously cached
                       // result to make promise empty again
        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_);
            // propagate coroutine exception in called context
 
            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); // max 94 before uint64_t overflows
 
        for (int j = 0; gen; ++j)
            std::cout << "fib(" << j << ")=" << gen() << '\n';
    }
    catch (const std::exception& ex)
    {
        std::cerr << "Exception: " << ex.what() << '\n';
    }
    catch (...)
    {
        std::cerr << "Unknown exception.\n";
    }
}

输出

fib(0)=0
fib(1)=1
fib(2)=1
fib(3)=2
fib(4)=3
fib(5)=5
fib(6)=8
fib(7)=13
fib(8)=21
fib(9)=34

[编辑] 注解

特性测试 标准 特性
__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++ 标准。

DR 应用于 已发布行为 正确行为
CWG 2556 C++20 无效的 return_void 使得
协程末尾的 fall-off 行为未定义
程序在此情况下是
非良构的
CWG 2668 C++20 co_await 不能出现在 lambda 表达式中 允许
CWG 2754 C++23 *this 在构造 promise 时被获取
用于显式对象成员函数
*this 在此情况下不被
获取

[编辑] 参见

(C++23)
一个表示同步协程生成器的 视图
(类模板) [编辑]

[编辑] 外部链接

1.  Lewis Baker, 2017-2022 - 非对称转移。
2.  David Mazières, 2021 - C++20 协程教程。
3.  Chuanqi Xu & Yu Qi & Yao Han, 2021 - C++20 协程原理与应用。(中文)
4.  Simon Tatham, 2023 - 编写自定义 C++20 协程系统。