什么是协程?
协程就是一段可以挂起(suspend)和恢复(resume)的程序,一般而言,就是一个支持挂起和恢复的函数。
一般情况下,函数一旦开始,就无法暂停。如果一个函数能够暂停,那它就可以被认为是我们开头提到的协程。所以挂起(suspend)就可以理解成暂停,恢复(resume)就理解成从暂停的地方继续执行。
Result Coroutine() {
std::cout << 1 << std::endl;
co_await std::suspend_always{};
std::cout << 2 << std::endl;
co_await std::suspend_always{};
std::cout << 3 << std::endl;
};
Result 是按照协程的规则定义的类型,之后再详细介绍。在 C++ 20当中,一个函数的返回值类型如果是符合协程的规则的类型,那么这个函数就是一个协程。
co_await std::suspend_always{};,其中 co_await 是个关键字,会使得当前函数(协程)的执行被挂起。
在控制台看到输出 1 以后,很可能过了很久才看到 2,这个“很久”也一般不是因为当前执行的线程被阻塞了,而是当前函数(协程)执行的位置被存起来,在将来某个时间点又读取出来继续执行的。
协程的状态
对应线程,协程也有执行,挂起,恢复,异常,返回状态。
C++ 协程会在开始执行时的第一步就使用 operator new 来开辟一块内存来存放这些信息,这块内存或者说这个对象又被称为协程的状态(coroutine state)。
协程的状态不仅会被用于存放挂起时的位置(后称为挂起点),也会在协程开始执行时存入协程体的参数值。例如:
Result Coroutine(int start_value) {
std::cout << start_value << std::endl;
co_await std::suspend_always{};
std::cout << start_value + 1 << std::endl;
};
这里的 start_value 就会被存入协程的状态当中。
需要注意的是,如果参数是值类型,他们的值或被移动或被复制(取决于类型自身的复制构造和移动构造的定义)到协程的状态当中;如果是引用、指针类型,那么存入协程的状态的值将会是引用或指针本身,而不是其指向的对象,这时候需要开发者自行保证协程在挂起后续恢复执行时参数引用或者指针指向的对象仍然存活。
与创建相对应,在协程执行完成或者被外部主动销毁之后,协程的状态也随之被销毁释放(编译器处理,不需要显式调用)。
协程的挂起
C++ 通过 co_await 表达式来处理协程的挂起,表达式的操作对象则为等待体(awaiter)。
等待体需要实现三个函数,这三个函数在挂起和恢复时分别调用。
await_ready
标准库当中提供了两个非常简单直接的等待体,struct suspend_always 表示总是挂起,struct suspend_never 表示总是不挂起。这二者的功能主要就是依赖 await_ready 函数的返回值:
struct suspend_never {
constexpr bool await_ready() const noexcept {
return true; // 返回 true,总是不挂起
}
//等待体其他内容...
};
struct suspend_always {
constexpr bool await_ready() const noexcept {
return false; // 返回 false,总是挂起
}
//等待体其他内容...
};
await_suspend
await_ready 返回 false 时,协程就挂起了。这时候协程的局部变量和挂起点都会被存入协程的状态当中,await_suspend 被调用到。
返回值 await_suspend(std::coroutine_handle<> coroutine_handle);
参数 coroutine_handle 用来表示当前协程,可以在稍后合适的时机通过调用 resume 来恢复执行当前协程:
coroutine_handle.resume();
await_suspend 函数的返回值类型没有明确给出,因为它有以下几种选项:
- 返回 void 类型或者返回 true,表示当前协程挂起之后将执行权还给当初调用或者恢复当前协程的函数。
- 返回 false,则恢复执行当前协程。注意此时不同于 await_ready 返回 true 的情形,此时协程已经挂起,await_suspend 返回 false 相当于挂起又立即恢复。
- 返回其他协程的 coroutine_handle 对象,这时候返回的 coroutine_handle 对应的协程被恢复执行。
- 抛出异常,此时当前协程恢复执行,并在当前协程当中抛出异常。
await_resume
同样地,await_resume 的返回值类型也是不限定的,返回值将作为 co_await 表达式的返回值。
等待体示例
struct Awaiter {
int value;
bool await_ready() {
// 协程挂起
return false;
}
void await_suspend(std::coroutine_handle<> coroutine_handle) {
// 切换线程
std::async([=](){
using namespace std::chrono_literals;
// sleep 1s
std::this_thread::sleep_for(1s);
// 恢复协程
coroutine_handle.resume();
});
}
int await_resume() {
// value 将作为 co_await 表达式的值
return value;
}
};
await_ready:该方法用来检查协程是否需要挂起。如果返回 false,表示协程应该挂起,进入等待状态。
await_suspend:这个方法在 await_ready 返回 false 时被调用,用于挂起协程。
本样例的具体操作如下:
- 使用 std::async 创建一个异步任务,该任务会在一个独立的线程上执行。
- 在这个线程中,首先使线程暂停一秒钟(std::this_thread::sleep_for(1s))。
- 暂停后,调用 coroutine_handle.resume() 以恢复被挂起的协程。
通过这种方式,await_suspend 方法实现了将协程暂停一段时间的效果,并且不阻塞协程所在的主线程。
await_resume:这个方法在协程恢复执行后被调用,用于传递协程的结果。在这个例子中,它返回结构体中的 value 成员变量,该变量将作为 co_await 表达式的结果。
协程的返回值类型
区别一个函数是不是协程,是通过它的返回值类型来判断的。如果它的返回值类型满足协程的规则,那这个函数就会被编译成协程。
规则就是返回值类型能够实例化下面的模板类型 _Coroutine_traits。
其再#include 文件中有定义如下
template <class _Ret, class = void>
struct _Coroutine_traits {};
template <class _Ret>
struct _Coroutine_traits<_Ret, void_t<typename _Ret::promise_type>> {
using promise_type = typename _Ret::promise_type;
};
template <class _Ret, class...>
struct coroutine_traits : _Coroutine_traits<_Ret> {};
简单来说,就是返回值类型 _Ret 能够找到一个类型 _Ret::promise_type 与之相匹配。这个 promise_type 既可以是直接定义在 _Ret 当中的类型,也可以通过 using 指向已经存在的其他外部类型。
此时,可以给出 Result 的部分实现:
struct Result {
struct promise_type {
//内部实现 见下文
};
};
协程返回值对象的构建
这时已经了解 C++ 当中如何界定一个协程。不过会产生一个新的问题,返回值是从哪儿来的?协程体当中并没有给出 Result 对象创建的代码。
实际上,Result 对象的创建是由 promise_type 负责的,需要定义一个 get_return_object 函数来处理对 Result 对象的创建:
struct Result {
struct promise_type {
Result get_return_object() {
// 创建 Result 对象
return {};
}
//其余函数
};
};
不同于一般的函数,协程的返回值并不是在返回之前才创建,而是在协程的状态创建出来之后马上就创建的。也就是说,协程的状态被创建出来之后,会立即构造 promise_type 对象,进而调用 get_return_object 来创建返回值对象。
promise_type 类型的构造函数参数列表如果与协程的参数列表一致,那么构造 promise_type 时就会调用这个构造函数。否则,就通过默认无参构造函数来构造 promise_type。
协程体的执行
initial_suspend
协程体执行的第一步是调用 co_await promise.initial_suspend(),initial_suspend 的返回值就是一个等待对象(awaiter),如果返回值满足挂起的条件,则协程体在最一开始就立即挂起。这个点实际上非常重要,可以通过控制 initial_suspend 返回的等待体来实现协程的执行调度。
有关调度的内容见专栏后续。
协程体的返回情况(值,void,异常)
这个协程的返回并不是返回函数声明前面的类型,而是使用co_return。这个声明的Result更像是沟通协程和调用者的一个桥梁。
接下来执行协程体。协程体当中会存在 co_await、co_yield、co_return 三种协程特有的调用,其中
- co_await 前面已经介绍过,用来将协程挂起。
- co_yield 则是 co_await 的另一种实现,用于传值给协程的调用者或恢复者或被恢复者
- co_return 则用来返回一个值或者从协程体返回。
对于返回一个值的情况,需要在 promise_type 当中定义一个函数
void return_value();//必须返回void
那么再调用co_return 时
co_return 1000;
1000 会作为参数传入,即 return_value 函数的参数 value 的值为 1000。这个值可以存到 promise_type 对象当中,外部的调用者可以获取到。
除了返回值的情况以外,C++ 协程当然也支持返回 void。只不过 promise_type 要定义的函数就不再是 return_value 了,而是 return_void 了:
struct Result {
struct promise_type {
void return_void() {
...
}
...
};
};
此时,协程内部就可以通过 co_return 来退出协程体了:
协程体除了正常返回以外,也可以抛出异常。异常实际上也是一种结果的类型,因此处理方式也与返回结果相似。我们只需要在 promise_type 当中定义一个函数,在异常抛出时这个函数就会被调用到:
struct Result {
struct promise_type {
void unhandled_exception() {
exception_ = std::current_exception(); // 获取当前异常
}
...
};
};
final_suspend
当协程执行完成或者抛出异常之后会先清理局部变量,接着调用 final_suspend 来方便开发者自行处理其他资源的销毁逻辑。final_suspend 也可以返回一个等待体使得当前协程挂起,但之后当前协程应当通过 coroutine_handle 的 destroy 函数来直接销毁,而不是 resume。
一般来说final_suspend应该挂起协程,希望其销毁和result(接受了协程返回值)生命周期一致,避免提前销毁出现意外。
int main() {
auto result=Coroutine();
return 0;
}
测试样例
#define __cpp_lib_coroutine
#include <iostream>
#include <coroutine>
#include <future>
#include <chrono>
using namespace std::chrono_literals;
void Fun() {
std::cout << 1 << std::endl;
std::cout << 2 << std::endl;
std::cout << 3 << std::endl;
std::cout << 4 << std::endl;
}
struct Result {
struct promise_type {
int value;
std::suspend_never initial_suspend() {
return {};
}
std::suspend_never final_suspend() noexcept {
return {};
}
Result get_return_object() {
return {};
}
void return_value(int _value) {
value = _value;
std::cout << "coroutine return " << value << '\n';
}
void unhandled_exception() {
}
};
};
struct Awaiter {
int value;
bool await_ready() {
return false;
}
void await_suspend(std::coroutine_handle<> coroutine_handle) {
std::async([=]() {
std::this_thread::sleep_for(1s);
coroutine_handle.resume();
});
}
int await_resume() {
return value;
}
};
Result Coroutine() {
std::cout << "Coroutine begin " << std::endl;
Fun();
std::cout << co_await Awaiter{.value = 1000} << std::endl;
co_return 5;
};
int main() {
auto result = Coroutine();
std::this_thread::sleep_for(1s);
//等待一下看协程返回结果
return 0;
}
输出结果
Coroutine begin
1
2
3
4
1000
coroutine return 5