协程
协程
协程是能暂停执行以在之后恢复的函数。协程是无栈的:它们通过返回到调用方暂停执行,并且恢复执行所需的数据与栈分离存储。

C++20以后,可以通过co_await或co_yield去挂起一个协程,通过co_return真正结束一个协程。
co_await用于暂停执行,直到恢复;co_yield用于暂停执行并返回一个值;co_return用于完成执行并返回一个值。
一个函数含有上述这些关键字时,那么这个函数就是一个协程。
演示
#include <iostream>
#include <coroutine>
using namespace std;
struct CoRoutineStructure {
struct promise_type {
suspend_never initial_suspend() {
cout << "coroutine initial suspend\n";
return {};
}
suspend_never final_suspend() noexcept {
cout << "coroutine final suspend\n";
return {};
}
CoRoutineStructure get_return_object() {
cout << "coroutine get_return_object\n";
return CoRoutineStructure(coroutine_handle<promise_type>::from_promise(*this));
}
void unhandled_exception() { }
};
public:
CoRoutineStructure(const coroutine_handle<promise_type>& handle) : m_handle(handle) {
cout << "construct CoRoutineStruct\n";
}
coroutine_handle<promise_type> m_handle;
};
struct Input {
constexpr bool await_ready() {
cout << "Input await ready\n";
return false;
}
constexpr void await_suspend(coroutine_handle<CoRoutineStructure::promise_type> h) {
cout << "Input await suspend\n";
}
constexpr int await_resume() {
cout << "Input await resume\n";
return 42;
}
};
CoRoutineStructure guessNumber() {
Input input;
int g = co_await input;
cout << "coroutine: guess " << g << endl;
}
int main() {
auto ret = guessNumber();
cout << "main: make a guess...\n";
ret.m_handle.resume();
return 0;
}
输出结果:
coroutine get_return_object
construct CoRoutineStruct
coroutine initial suspend
Input await ready
Input await suspend
main: make a guess...
Input await resume
coroutine: guess 42
coroutine final suspend
如果这段代码没有协程的参与,首先调用了getNumber函数,那么就应该先打印
coroutine: guess 42
函数执行完成后返回main函数,再打印后面一句
main: make a guess...
但加入了协程后,一切变得有些不一样了。我们在必要的函数中加入了打印来追踪代码执行的顺序,可以看到在有了协程参与后,程序自己“偷偷”做了很多的事情,最终使得先打印了main函数中的字符串,后打印了guessNumber函数中的字符串。
协程是C++20后才加入标准库中,需要显式加上
-std=c++20的选项才能正确编译(至少gcc13.2.0版本是这样)。
概念
执行
当协程启动时,会依次执行下述操作:
operator new分配协程状态对象;- 将所有的函数形参复制到协程状态中(拷贝、移动、引用);
- 调用承诺对象的构造函数,如果没有接收所有形参的构造函数,会调用默认构造;
- 调用承诺对象的
get_return_object,并将结果保存在局部变量中,会在协程首次暂停时返回给调用方; - 调用
initial_suspend方法,并co_wait它的结果; - 当
initial_suspend恢复时,开始协程体的执行。
promise_type
首先需要强行灌输一个C++协程的概念,C++的协程是通过一个内部定义了promise_type结构体的协程体来实现的,如果要使用协程,就需要在自定义的协程体中定义一个名为promise_type的结构体(后续称呼其为承诺对象),并依次提供initial_suspend、final_suspend、get_return_object、unhandled_exception等方法,这四个是必须的。
| 方法 | 说明 |
|---|---|
initial_suspend |
决定线程启动时是否立即挂起,标准库中提供了suspend_never和suspend_always分别表示从不挂起和总是挂起,这俩都是支持协程的类,后续我们的实现也要和它们类似,先按下不表。 |
final_suspend |
决定线程结束时是否要挂起,返回值也是一个支持协程操作的对象,同上。 |
get_return_object |
线程挂起时返回的协程体,也就是一个内部含有承诺对象的协程体,这里简单起见就用刚刚定义的这个协程体了,另外,协程体必须要通过一种方式来获取到协程执行过程的信息,标准库中提供了coroutine_handle句柄来封装承诺对象,所以协程体内都要定义一个这样的句柄成员。 |
unhandled_exception |
这个方法会在出现异常时被调用,demo中就定义为空函数了。 |
自定义的协程类
在guessNumber函数中,我们用co_await来启用协程等待Input这个协程对象。一个类要支持协程,必须提供await_ready、await_suspend、await_resume三个成员函数,正如Input一样。
| 方法 | 说明 |
|---|---|
await_ready |
这个方法会被co_await关键字调用。考查suspend_never和suspend_always的效果和实现可以推测,await_ready表示当前协程对象是否准备好了响应await,返回值是布尔类型,如果为true,表示当前的对象已经做好准备了响应wait了,那么就不需要再挂起当前的协程,而是继续执行await_resume方法;如果是false,表示当前的对象还没做好准备,那么就会调用await_suspend方法挂起该对象并切换到别的协程。 |
await_suspend |
在协程挂起时被调用,用于处理当前协程对象挂起时的一些处理操作。关键点在于入参是一个承诺对象的协程句柄,这个协程句柄保存了当前协程的信息,根据这个句柄获取协程的暂停状态和调度协程的恢复或销毁。await_suspend返回值如果是void或true,表示将控制权返回给当前协程的调用方/恢复方;如果是false,表示恢复当前协程; 如果返回的是某个某个协程的句柄,通过调用其resume方法恢复该协程。 |
await_resume |
当协程对象在等待后重新获得了协程的控制权,会执行该方法恢复挂起前的状态并继续执行,并且其返回值就是co_await expr中表达式的结果。 |
co_await
了解了必要的概念后,我们再完整地梳理一下co_await在协程中的工作逻辑:
- 首先,协程在一开始就创建了一个承诺对象,也就是我们自定义的
promise_type类对象,调用了它的initial_suspend方法来判断是否在协程一开始就挂起,我们返回的是suspend_never表示从不挂起; - 然后继续执行,调用get_return_object方法,返回了含有该承诺对象的句柄的协程体,它将会作为协程函数的返回值传递出去,也就是main函数中接收协程返回的ret;
- 下面是协程正式执行,运行到了
co_await处,等待的是一个我们自定义的协程对象Input,co_await的执行逻辑就是先判断await_ready方法的返回值决定是否挂起,我们返回的是false,表示挂起; - 挂起后执行await_suspend方法,将当前协程信息保存在了入参的承诺对象句柄中,
co_await处于等待状态,协程挂起并回到协程的调用者,也就是main函数; - main函数获取到了传递出来的协程体对象,开始执行调用协程的下一行,打印一行文字,然后使用协程体对象中保存了协程的承诺对象的句柄成员,调用其resume方法唤醒协程;
- 协程结束等待,调用await_resume恢复协程的执行,这里我们是将一个整形作为结果传递出去,到这才完成了
co_await的一个完整流程,成功给等待co_await赋值的变量g赋上了值,即g等于await_resume的返回值42; - 最后协程打印了结果,完成整个协程的执行并退出。
co_yield
co_yield类似于一个语法糖,等价于
co_await promise.yield_value(表达式)
要使用co_yield,在承诺对象中就要增加一个方法(也可以是不挂起):
suspend_always yield_value(表达式);
例如稍微修改下上面的代码:
int number = 0;
struct CoRoutineStructure {
struct promise_type {
// ...
suspend_always yield_value(int val) {
number = val;
return {};
}
};
// ...
};
CoRoutineStructure guessNumber() {
cout << "coroutine function\n";
Input input;
int g = co_await input;
cout << "coroutine: guess " << g << endl;
co_yield 13;
}
int main() {
auto ret = guessNumber();
cout << "main: make a guess...\n";
ret.m_handle.resume();
cout << "main: guess again...\n";
cout << "number: " << number << endl;
ret.m_handle.resume();
return 0;
}
能得到下面的输出:
coroutine get_return_object
construct CoRoutineStruct
coroutine initial suspend
coroutine function
Input await ready
Input await suspend
main: make a guess...
Input await resume
coroutine: guess 42
main: guess again...
number: 13
coroutine final suspend
co_return
co_return表示协程的结束,类似于co_yield,可以返回void或者是一个值,分别要在承诺对象内声明return_void和return_value(表达式)。
完成猜数字游戏
在main函数中开启猜数字游戏,协程扮演玩家,每猜一个数字就交给main函数判断一下是否猜对了,直至猜中为止。
#include <iostream>
#include <random>
#include <coroutine>
#include <array>
using namespace std;
enum class Status : char {
start,
correct,
less,
greater,
};
class Judge {
public:
Judge(int answer, int min, int max) : m_answer(answer), min(min), max(max) { }
Status operator()() {
if(m_guess == m_answer) {
stat = Status::correct;
}
else if(m_guess < m_answer) {
stat = Status::less;
}
else if(m_guess > m_answer) {
stat = Status::greater;
}
return stat;
}
Status stat = Status::start;
int min;
int max;
int m_guess = -1;
private:
int m_answer;
};
class CoRoutine {
public:
struct promise_type {
promise_type(Judge& j) : judge(j) { }
suspend_never initial_suspend() {
return {};
}
CoRoutine get_return_object() {
return CoRoutine(coroutine_handle<promise_type>::from_promise(*this));
}
suspend_never final_suspend() noexcept {
return {};
}
suspend_always yield_value(int val) {
judge.m_guess = val;
return {};
}
void return_void() { }
void unhandled_exception() { }
private:
Judge& judge;
};
public:
CoRoutine(const coroutine_handle<promise_type>& handle) : m_handle(handle) { }
coroutine_handle<promise_type> m_handle;
};
CoRoutine guess(Judge& judge) {
std::array<int, 2> range{ judge.min, judge.max };
int number;
while(judge.stat != Status::correct) {
if(judge.stat == Status::less) {
range[0] = number + 1;
}
else if(judge.stat == Status::greater) {
range[1] = number - 1;
}
number = (range[0] + range[1]) / 2;
cout << "guess number: " << number << endl;
co_yield number;
}
co_return;
}
int main() {
random_device seed;
default_random_engine eng(seed());
int answer = uniform_int_distribution<int>(1, 100)(eng);
cout << "guess a number between 1 and 100.\n";
Judge judge(answer, 1, 100);
auto coroutine = guess(judge);
while(true) {
auto result = judge();
if(result == Status::correct) {
cout << "you're right!" << endl;
coroutine.m_handle.destroy();
break;
}
else if(result == Status::less) {
cout << "miss, less than answer.\n";
}
else if(result == Status::greater) {
cout << "miss, greater than answer.\n";
}
coroutine.m_handle.resume();
}
return 0;
}
我们额外定义了一个Judge类来判断每次猜的数字是否正确,协程每次猜一个数字,通过co_yield发送给judge进行判断,猜中了结束,猜错了main函数调用承诺对象的句柄的resume方法恢复协程,重猜一个数字。

浙公网安备 33010602011771号