C++20 协程参考手册详解 - 源自 cppreference.com
人话版
先说“人说”,简化版本,更易理解。
宏观概念:协程是一个可以暂定和恢复执行的函数。(普通函数是线程相关的,函数的调用依赖于线程栈,而协程的运行状态暂定状态保存在堆区,协程在线程A暂停后,后续可以在另一个线程B继续上次的状态执行。)
具体的:在C++中,只要函数出现了co_await
,co_yield
或co_return
这三个关键字之一,函数就由编译器变为协程。
co_await
co_await
的作用是让协程“暂停一下”,等待某个操作(比如网络请求或文件读取)完成后,再继续执行。co_await
就是这个“等一等”的动作,暂停协程,干别的事,等条件满足再回来。
但问题来了:如果你直接对一个自定义类型用co_await
,比如co_await IntReader{}
,编译器会一脸懵逼。它不知道这个类型啥时候算“完成”,也不知道结果在哪儿。为了让co_await
能用,我们需要让自定义类型遵守一个规则,这个规则叫 Awaitable
。
Awaitable
就像一份“协程使用说明书”,告诉编译器怎么处理暂停和恢复。它要求你的类型实现三个关键函数:
bool await_ready()
:告诉协程“现在能不能直接执行?”- 返回类型:
bool
。 - 作用:在执行
co_awit
时,先执行这个函数检测是否可以立即执行,避免不必要的暂停。
- 返回类型:
void await_suspend(std::coroutine_handle<> h)
:如果要暂停,接下来该干啥?- 返回类型:可以是
void
;也可以是bool
,ture
表暂停,false
表不暂停。 - 参数:
coroutine_handle<>
协程的“遥控器”。本质上,std::coroutine_handle
是一个指向堆上分配的协程帧(coroutine frame)的、轻量级的指针。std::coroutine_handle<>
(通用遥控器),它可以指向任何类型的协程,无论其promise_type
是什么,因为它不需要关心挂起的是哪种协程,它只需要一个通用的句柄,以便之后能调用.resume()
即可。coroutine_handle<PromiseType>
(专用遥控器)因为编译器知道 Promise 的具体类型,所以你可以安全地调用.promise()
方法,获得对PromiseType
对象的引用,然后从中取出结果。 - 作用:如果协程要暂停会调用
await_suspend()
,通过参数拿到协程句柄(指向当前暂定的协程实例),可以在未来某个时刻“唤醒”协程(h.resume()
)。
- 返回类型:可以是
void await_resume()
:恢复时,返回什么结果?- 返回类型:可以是
void
,也可以是具体类型 - 作用:当协程恢复执行时(或压根没暂停)被调用,其返回值就是
co_awit
表达式的结果。
- 返回类型:可以是
- 这三个函数一起合作,让
co_await
知道如何暂停、等待和继续
initial_suspend
和final_suspend
相当于协程生命周期开始和结束时的两个“守门人”,它们通过返回一个 Awaitable 对象(通常是 std::suspend_always
或 std::suspend_never
)来决定协程在两个关键时刻的行为:
initial_suspend
:执行您编写的任何一行协程体代码之前,initial_suspend
会被自动调用并co_await
,它回答了一个关键问题:“协程被调用后**,**是应该立刻开始执行,还是应该创建一个‘暂停’的任务,等待调用者明确命令才开始?”final_suspend
:当协程的函数体执行完毕(无论是通过co_return
正常结束,还是执行到函数末尾隐式结束)并且所有局部变量都已被析构之后,final_suspend
会被自动调用并co_await
。它回答了另一个关键问题:“协程执行完毕后,是应该立即自我销毁,还是应该停留在‘已完成’的状态,等待外界来处理它的‘后事’(比如读取返回值)?”
#include <iostream>
#include <coroutine>
#include <thread>
#include <unistd.h>class IntReader{
public:bool await_ready() { return false;}void await_suspend(std::coroutine_handle<> handle){std::thread thread([this,handle](){sleep(1);value_ = 1;handle.resume();});thread.detach();}int await_resume(){return value_;}
private:int value_;};// Task这里先不用管,只需要关注上面的IntReader类
class Task;
class Task{
public:class promise_type{public:Task get_return_object() { return {}; }std::suspend_never initial_suspend() { return {}; }std::suspend_never final_suspend() noexcept { return {}; }void unhandled_exception() {}void return_void() {}};
};Task PrintInt(){IntReader reader1;int total = co_await reader1;IntReader reader2;total += co_await reader2;IntReader reader3;total += co_await reader3;std::cout<<total<<std::endl;
}
int main(){PrintInt();getchar();return 0;
}
co_return
协程的返回类型要求
C++对协程的返回类型只有一个硬性规定:它必须包含一个名为promise_type
的内嵌类型
当你调用一个协程函数时:
- 编译器会在堆上分配空间,保存协程的状态
- 同时创建一个
promise_type
对象,嵌在返回类型里 - 通过
promise_type
定义的函数,你可以控制协程的行为,或者与调用者交换数据。
promise_type的核心函数:get_return_object()
promise_type
必须实现一个函数:get_return_object()
。它的作用是创建协程的返回值对象。
调用时机:
- 在协程函数被调用时,编译器会先创建
promise_typer
对象,然后调用get_return_object()
,生成返回类型(比如Task
)给调用者。 promise_type
是返回类型的内嵌类型,但编译器不会直接构造返回类型,而是通过promise_type
来“间接”生成它。
返回值的作用:
- 取决于设计者的意图。如果只是想让协程干活(比如打印),返回值可以是个空壳。
- 如果想让协程返回数据, 就要在返回类型里设计获取数据的接口。
#include <iostream>
#include <coroutine>
#include <memory>
#include <thread>
#include <unistd.h>class IntReader{
public:bool await_ready() { return false;}void await_suspend(std::coroutine_handle<> handle){std::thread thread([this,handle](){sleep(1);value_ = 1;handle.resume();});thread.detach();}int await_resume(){return value_;}
private:int value_;};class Task;
class Task{
public:class promise_type{public:promise_type(): value_(std::make_shared<int>()) {}Task get_return_object() { return Task{value_}; }std::suspend_never initial_suspend() { return {}; }std::suspend_never final_suspend() noexcept { return {}; }void unhandled_exception() {}void return_value(int value){*value_=value;}private:std::shared_ptr<int> value_;};Task(const std::shared_ptr<int>& value): value_(value) {}int GetValue() const{return *value_;}}
private:std::shared_ptr<int> value_;};Task GetInt(){IntReader reader1;int total = co_await reader1;IntReader reader2;total += co_await reader2;IntReader reader3;total += co_await reader3;co_return total;
}
int main(){auto task = GetInt();sleep(4); // 这里为了方便,直接等4秒std::cout<<task.GetValue()<<std::endl;getchar();return 0;
}
co_yield
co_yield
表达式向调用者返回一个值并暂停当前协程:它是可恢复生成器 (generator) 函数的常见构建块。
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 {// 类名 'Generator' 是我们的选择,对于协程魔法来说不是必需的。// 编译器通过 'co_yield' 关键字的存在来识别协程。// 你可以使用 'MyGenerator' (或任何其他名字),只要你包含// 一个带有 'MyGenerator get_return_object()' 方法的嵌套结构体 promise_type。// (注意:重命名时需要调整构造函数和析构函数的声明。)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 conceptstd::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(); // 要可靠地判断协程是否结束,// 以及是否将通过C++的getter(下方的operator())在协程中生成下一个值(co_yield),// 唯一的方法是执行/恢复协程直到下一个co_yield点(或让它执行完毕)。// 然后我们将结果存储/缓存在 promise 中,以允许 getter (下方的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("斐波那契序列太大,元素会溢出。");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 溢出前最大为 94for (int j = 0; gen; ++j)std::cout << "fib(" << j << ")=" << gen() << '\n';}catch (const std::exception& ex) {std::cerr << "异常: " << ex.what() << '\n';}catch (...) {std::cerr << "未知异常。\n";}
}
执行流程
每个协程都与以下三者相关联:
- Promise 对象 (the promise object):在协程内部被操纵。协程通过此对象提交其结果或异常。Promise 对象与
std::promise
没有任何关系。 - 协程句柄 (the coroutine handle):在协程外部被操纵。这是一个**非拥有式(non-owning)**的句柄,用于恢复协程的执行或销毁协程帧。
- 协程状态 (the coroutine state):这是一个内部的、动态分配(除非分配被优化掉)的对象,它包含:
- 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()
恢复时,开始执行协程的主体部分。
当协程到达一个暂停点时:
- 先前获得的返回对象会被返回给调用者/恢复者(如有必要,会进行到协程返回类型的隐式转换)。
当协程到达 co_return
语句时,它会执行以下操作:
- 对于
co_return;
或co_return expr;
(其中expr
类型为void
),调用promise.return_void()
。 - 对于
co_return expr;
(其中expr
类型非void
),调用promise.return_value(expr)
。 - 按创建顺序的逆序,销毁所有具有自动存储期的变量。
- 调用
promise.final_suspend()
并co_await
其结果。
如果协程执行完函数体末尾而没有 co_return
,这等价于 co_return;
,但如果 Promise
域中找不到 return_void
的声明,则行为是未定义的。一个在其函数体内没有任何协程定义关键字的函数不是协程,无论其返回类型如何,如果其返回类型不是 void
(可有 cv 限定),则执行到函数末尾会导致未定义行为。
如果协程因未捕获的异常而结束,它会执行以下操作:
- 捕获异常并在
catch
块内调用promise.unhandled_exception()
。 - 调用
promise.final_suspend()
并co_await
其结果(例如,用于恢复一个延续或发布一个结果)。从此时恢复协程是未定义行为。
当协程状态被销毁时(无论是通过 co_return
、未捕获的异常终止,还是通过其句柄销毁),它会执行以下操作:
- 调用 Promise 对象的析构函数。
- 调用函数参数副本的析构函数。
- 调用
operator delete
来释放协程状态所使用的内存。 - 将执行权交还给调用者/恢复者。
以上“人话版”是看完视频后的总结于扩展。
官方手册
引言
本篇内容是 en.cppreference.com Coroutines (C++20) 页面的完整中文翻译,旨在为需要精确参考的 C++ 开发者提供一份详尽的中文对应文档。
协程 (C++20)
协程是一种可以暂停执行以便后续恢复的函数。协程是无栈的(stackless):它们通过返回至调用者来暂停执行,而恢复执行所需的数据与栈分离存储。这使得顺序执行的代码可以异步执行(例如,无需显式回调即可处理非阻塞 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; }
每个协程都必须有一个满足下述多项要求的返回类型。
限制 (Restrictions)
- 协程不能使用可变参数(variadic arguments)、普通的
return
语句或占位符返回类型(auto
或Concept
)。 consteval
函数、constexpr
函数、构造函数、析构函数以及main
函数不能是协程。
执行 (Execution)
每个协程都与以下三者相关联:
- Promise 对象 (the promise object):在协程内部被操纵。协程通过此对象提交其结果或异常。Promise 对象与
std::promise
没有任何关系。 - 协程句柄 (the coroutine handle):在协程外部被操纵。这是一个**非拥有式(non-owning)**的句柄,用于恢复协程的执行或销毁协程帧。
- 协程状态 (the coroutine state):这是一个内部的、动态分配(除非分配被优化掉)的对象,它包含:
- 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} 已销毁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 作为一个按值传递的参数已被复制到协程帧中h.destroy();
}
当协程到达一个暂停点时:
- 先前获得的返回对象会被返回给调用者/恢复者(如有必要,会进行到协程返回类型的隐式转换)。
当协程到达 co_return
语句时,它会执行以下操作:
- 对于
co_return;
或co_return expr;
(其中expr
类型为void
),调用promise.return_void()
。 - 对于
co_return expr;
(其中expr
类型非void
),调用promise.return_value(expr)
。 - 按创建顺序的逆序,销毁所有具有自动存储期的变量。
- 调用
promise.final_suspend()
并co_await
其结果。
如果协程执行完函数体末尾而没有 co_return
,这等价于 co_return;
,但如果 Promise
域中找不到 return_void
的声明,则行为是未定义的。一个在其函数体内没有任何协程定义关键字的函数不是协程,无论其返回类型如何,如果其返回类型不是 void
(可有 cv 限定),则执行到函数末尾会导致未定义行为。
// 假设 task 是某种协程任务类型
task<void> f() {// 不是协程,未定义行为
}task<void> g() {co_return; // OK
}task<void> h() {co_await g();// OK,隐式的 co_return;
}
如果协程因未捕获的异常而结束,它会执行以下操作:
- 捕获异常并在
catch
块内调用promise.unhandled_exception()
。 - 调用
promise.final_suspend()
并co_await
其结果(例如,用于恢复一个延续或发布一个结果)。从此时恢复协程是未定义行为。
当协程状态被销毁时(无论是通过 co_return
、未捕获的异常终止,还是通过其句柄销毁),它会执行以下操作:
- 调用 Promise 对象的析构函数。
- 调用函数参数副本的析构函数。
- 调用
operator delete
来释放协程状态所使用的内存。 - 将执行权交还给调用者/恢复者。
动态分配 (Dynamic allocation)
协程状态通过非数组形式的 operator new
进行动态分配。
-
如果
Promise
类型定义了类级别的替换,则会使用它;否则,使用全局的operator new
。 -
如果
Promise
类型定义了接受额外参数的放置式operator new
,并且这些参数与一个参数列表匹配(第一个参数是请求的大小std::size_t
,其余是协程函数的参数),那么这些参数将被传递给operator new
(这使得对协程使用前置分配器约定 (leading-allocator-convention) 成为可能)。 -
如果以下条件满足,对
operator new
的调用可以被优化掉(即使使用了自定义分配器):- 协程状态的生命周期严格嵌套在调用者的生命周期内,并且
- 协程帧的大小在调用点已知。
-
在这种情况下,协程状态被嵌入在调用者的栈帧(如果调用者是普通函数)或协程状态(如果调用者是协程)中。
-
如果分配失败,协程会抛出
std::bad_alloc
,除非Promise
类型定义了成员函数Promise::get_return_object_on_allocation_failure()
。如果定义了该函数,分配将使用nothrow
形式的operator new
,并且在分配失败时,协程会立即将从Promise::get_return_object_on_allocation_failure()
获得的对象返回给调用者。例如:struct Coroutine::promise_type {/* ... */// 确保使用不会抛出异常的 operator-newstatic Coroutine get_return_object_on_allocation_failure() {std::cerr << __func__ << '\n';throw std::bad_alloc(); // 或者, return 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
类型由以下方式确定:
std::coroutine_traits<R, Args...>::promise_type
,如果协程不是隐式对象成员函数。std::coroutine_traits<R, cv ClassT&, Args...>::promise_type
,如果协程是左值引用限定的隐式对象成员函数。std::coroutine_traits<R, cv ClassT&&, Args...>::promise_type
,如果协程是右值引用限定的隐式对象成员函数。
例如:
如果协程定义为… | 那么其 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 |
好的,这是您指定的 cppreference.com
协程页面后续内容的完整、忠实翻译。
co_await
一元运算符 co_await
暂停协程并将控制权返回给调用者。
co_await expr
co_await
表达式只能出现在常规函数体(包括 lambda 表达式的函数体)内的潜在求值表达式 (potentially-evaluated expression) 中,且不能出现在:
- handler 中。
declaration
语句中,除非它出现在该声明的初始化器中。init-statement
的simple-declaration
中(参见if
、switch
、for
和range-for
),除非它出现在该init-statement
的初始化器中。- 默认参数中。
- 具有静态或线程存储期的块作用域变量的初始化器中。
- (C++26 起)
co_await
表达式不能是契约断言 (contract assertion) 谓词的潜在求值子表达式。
首先,expr
被转换为一个可等待对象 (awaitable),如下所示:
- 如果
expr
来自初始暂停点、最终暂停点或yield
表达式,则 awaitable 就是expr
本身。 - 否则,如果当前协程的
Promise
类型有成员函数await_transform
,则 awaitable 是promise.await_transform(expr)
的结果。 - 否则,awaitable 就是
expr
本身。
然后,获得 awaiter 对象,如下所示:
- 如果
operator co_await
的重载决议给出了唯一的最佳重载,则 awaiter 是该调用的结果:- 对于成员重载:
awaitable.operator co_await()
- 对于非成员重载:
operator co_await(static_cast<Awaitable&&>(awaitable))
- 对于成员重载:
- 否则,如果没有找到
operator co_await
,则 awaiter 就是 awaitable 本身。 - 否则,如果重载决议有歧义,则程序是病态的 (ill-formed)。
如果上述表达式是一个右值(prvalue),则 awaiter 对象是从它物化(materialized)出的一个临时对象。否则,如果表达式是一个泛左值(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 semantics),而恢复者应该至少使用获取语义 (acquire semantics)。)例如,协程句柄可以放入一个回调中,在异步 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(); });// 潜在的未定义行为: 访问可能已被销毁的 *this// std::cout << "New thread ID: " << p_out->get_id() << '\n';std::cout << "New thread ID: " << out.get_id() << '\n'; // 这样是 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 在此处被销毁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_always
和 std::suspend_never
。
好的,遵照您的指示,这是对您提供的剩余部分内容的完整、忠实的中文翻译和博客形式的重写。
示例:promise_type::await_transform
和程序提供的 Awaiter
这个例子演示了 promise_type
如何通过 await_transform
成员函数来“拦截”并转换一个 co_await
表达式,返回一个自定义的 awaiter,从而实现对协程暂停行为的动态控制。
#include <cassert>
#include <coroutine>
#include <iostream>struct tunable_coro {// 一个 Awaiter,其“就绪”状态由构造函数的参数决定。class tunable_awaiter {bool ready_;public:explicit(false) tunable_awaiter(bool ready) : ready_{ready} {}// 三个标准的 awaiter 接口函数: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(); }// 一个用户提供的转换函数,它返回自定义的 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); }// 为简化起见,将这4个特殊成员函数声明为 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 << ' ';// 传递给 co_await 的 awaiter 会进入 promise_type::await_transform,// 它会发出一个 tunable_awaiter,这个 awaiter 最初会导致挂起(在每次迭代时返回到 main),// 但在调用 disable_suspension 之后,就不会再发生挂起,// 循环会一直运行到结束而不会返回到 main()。co_await std::suspend_always{};}
}int main() {auto coro = generate(8);coro(); // 只会发出第一个元素 == 0for (int k{}; k < 4; ++k) {coro(); // 每次迭代发出 1 2 3 4 中的一个std::cout << ": ";}coro.disable_suspension();coro(); // 一次性发出剩余的数字 5 6 7
}
输出:
0 1 : 2 : 3 : 4 : 5 6 7
co_yield
co_yield
表达式向调用者返回一个值并暂停当前协程:它是可恢复生成器 (generator) 函数的常见构建块。
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 {// 类名 'Generator' 是我们的选择,对于协程魔法来说不是必需的。// 编译器通过 'co_yield' 关键字的存在来识别协程。// 你可以使用 'MyGenerator' (或任何其他名字),只要你包含// 一个带有 'MyGenerator get_return_object()' 方法的嵌套结构体 promise_type。// (注意:重命名时需要调整构造函数和析构函数的声明。)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 conceptstd::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(); // 要可靠地判断协程是否结束,// 以及是否将通过C++的getter(下方的operator())在协程中生成下一个值(co_yield),// 唯一的方法是执行/恢复协程直到下一个co_yield点(或让它执行完毕)。// 然后我们将结果存储/缓存在 promise 中,以允许 getter (下方的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("斐波那契序列太大,元素会溢出。");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 溢出前最大为 94for (int j = 0; gen; ++j)std::cout << "fib(" << j << ")=" << gen() << '\n';}catch (const std::exception& ex) {std::cerr << "异常: " << ex.what() << '\n';}catch (...) {std::cerr << "未知异常。\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
注解 (Notes)
特性测试宏
特性 | 宏 | 值 | 标准 |
---|---|---|---|
Coroutines (编译器支持) | __cpp_impl_coroutine | 201902L | (C++20) |
Coroutines (库支持) | __cpp_lib_coroutine | 201902L | (C++20) |
std::generator | __cpp_lib_generator | 202207L | (C++23) |
关键字
co_await
, co_return
, co_yield
库支持
协程支持库定义了几个类型,为协程提供编译时和运行时的支持。
缺陷报告 (Defect reports)
以下行为变更的缺陷报告被追溯应用于先前发布的C++标准。
DR | 应用于 | 发布时的行为 | 正确的行为 |
---|---|---|---|
CWG 2556 | C++20 | 无效的 return_void 导致函数末尾结束的行为未定义 | 在此情况下程序是病态的 (ill-formed) |
CWG 2668 | C++20 | co_await 不能出现在 lambda 表达式中 | 允许 |
CWG 2754 | C++23 | 为显式对象成员函数构造 promise 对象时会捕获 *this | 在此情况下不捕获 *this |