C++ 20 Coroutine
C++20四大件之——协程(coroutine)
简述
协程是用户态的线程,内核不知道协程的存在,其上下文的切换完全由应用程序自身决定,而不需要内核的参与,因此其性能会比线程更好
线程需要依赖于操作系统,比如linux中依赖于线程库pthread
;但是协程不需要操作系统的支持,其完全由应用程序自己来实现
coroutine
c++20新增了coroutine ,coroutine没有显式的声明,任何包含co_await/co_yield/co_return
的函数都能够成为协程,一个典型的coroutine包含以下几个要素
co_await/co_yield/co_return
- returnType,即函数的返回类型,该返回类型中需要包含名为
promise_type
(名称必须保持一致)的嵌套类型 - promise_type一般用于在协程内部维护一些数据,处理异常
coroutine_handle<>
- 协程的句柄,给协程的调用者访问协程
- 假设定义
coroutine_handle<> h
,则可以通过h()
来激活(invoke)协程,h()
等价于h.resume()
- 通过h.destroy()来释放协程所占用的内存空间
co_await和awaiter(awaitable object)
- 对于co_await而言,其等待的对象被称为awaiter或者(awaitable object),该对象决定了在执行co_await时协程是否要被挂起,以及后续的一些步骤
- awaiter必须包含3个函数,
await_ready
、await_suspend
、await_resume
,其调用顺序如下- 首先调用
await_ready
来判断原始协程是否需要被挂起,如果await_ready
返回false
,则继续执行await_suspend
,本质上是通过awaiter
调用await_suspend
方法,即awaiter.await_suspend(h)
,h
为当前协程的句柄 - 如果
await_suspend
的返回类型为bool
类型,则需要进一步判断await_suspend
的返回值是否为true
,如果同时满足await_ready
返回值为false
以及await_suspend
返回值为true
,则协程会被挂起,直到被invoke - 在恢复控制权到协程前,会执行
await_resume
- 首先调用
<coroutine>
中给出了两个默认的awaiter
实现,即std::suspend_always
和std::suspend_never
,顾名思义,前者总是会挂起当前协程,而后者不会挂起当前协程
promise object
上面说到协程的返回类型必须要有一个名为
promise_type
的嵌套类型,promise_type
中包含名为get_return_object
的函数,其返回类型为外部类型ReturnType,即返回ReturnType类型的对象get_returun_object
在首次调用协程时被调用,将返回对象给予协程的调用者, 使得调用者得以访问访问协程一般来说返回类型会定义类型转化运算符,用于将当前返回类型转化为
std::coroutine_handle<promise_type>
类型,从而让调用者直接获取协程的句柄如果协程句柄为
std::coroutine_handle<promise_type>
,可以通过h.promise()
获取promise object,如果是类型实参为空,则没有该promise()
方法如果要在
promise_type
内部获取协程的句柄,可以通过静态函数std::coroutine_handle<promise_type>::from_promise(*this)
来获取其他方法
initial_suspend()
:指定协程在启动时是否被挂起,返回std::suspend_always
则挂起,返回std::suspend_never
则不挂起1 2
std::suspend_always initial_suspend(); std::suspend_never initial_suspend();
final_suspend()
:指定协程结束前是否挂起,挂起则可以通过句柄获取返回结果,否则直接将协程销毁1 2
std::suspend_always final_suspend() noexcept; std::suspend_never final_suspend() noexcept;
unhandled_exception()
:协程内部发生未捕获异常时,调用此函数处理异常,通常将异常存储到std::exception_ptr
中,供调用方访问。1
void unhandled_exception();
return_void()
:当协程中有co_return;
时需要有该方法,调用co_return
时会调用此方法1
void return_void();
return_value(valueType value)
:当协程中有co_return value;
时需要有该方法1
void return_value(valueType value);
yield_value(valueType value)
:当协程中有co_yield value;
时需要有该方法
例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <coroutine>
#include <iostream>
template <typename PromiseType>
// 用于返回promise地址的awaiter
struct GetPromise
{
PromiseType *_p;
bool await_ready() { return false; }
// 本质上就是要捕获这个句柄h,然后额外定义了一个awaiter来获取协程句柄,然后通过协程句柄获取promise object
bool await_suspend(std::coroutine_handle<PromiseType> h) // 通过协程句柄获取promise object
{
_p = &h.promise();
return false; // 只有当await_ready返回false且await_suspend返回false时,协程才会被挂起
}
PromiseType *await_resume() { return _p; } // 控制权恢复到协程时调用,返回指向协程句柄的promise object的指针
};
struct ReturnObject3
{
struct promise_type
{
unsigned* value_;
ReturnObject3 get_return_object()
{
return ReturnObject3{
.h_ = std::coroutine_handle<promise_type>::from_promise(*this)}; // 由promise_type获取其所归属的协程句柄
}
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void unhandled_exception() {}
};
std::coroutine_handle<promise_type> h_;
operator std::coroutine_handle<promise_type>() const { return h_; } // 类型转换运算符重载,将ReturnObject3转化为std::coroutine_handle<promise_type>
};
ReturnObject3 counter3()
{
auto pp = co_await GetPromise<ReturnObject3::promise_type>{}; // 这里通过非阻滞的方式获取当前协程的promise object,具体执行过程如下
/**
* 首先调用GetPromise::await_ready()检查是否需要挂起当前协程
* 然后调用GetPromise::await_suspend(h),h为当前协程counter3的协程句柄
* 由于该函数返回值为false,因此协程counter3不会被挂起,则将控制权恢复到协程counter3
* 恢复控制权前执行GetPromise::await_resume(),返回指向协程的promise obeject的指针
* 最后将控制权转交给协程counter3
*/
for (unsigned i = 0;; ++i)
{
pp->value_ = &i;
co_await std::suspend_always{}; // <coroutine>中默认的挂起awaiter,其默认会挂起当前协程,等待协程下一次被激活(invoke)
}
}
int main()
{
std::coroutine_handle<ReturnObject3::promise_type> h = counter3(); // 调用了类型转换运算符,由ReturnObject3类型转化到std::coroutine_handle<ReturnObject3::promise_type>
ReturnObject3::promise_type &promise = h.promise();
for (int i = 0; i < 3; ++i)
{
std::cout << "counter3: " << *promise.value_ << std::endl;
h(); // 通过协程句柄h,invoke协程h
}
h.destroy(); // 释放协程的内存空间(能否用RAII封装或者智能指针),不释放会内存泄漏(memory leak)
}
co_yield
上述实现过于臃肿,为了在协程中获取promise object,我们额外定义了awaiter
GetPromise
用于获取当前协程的句柄,然后通过此句柄调用promise()
方法来获取promise object实际上,大可不必这么麻烦,通过
co_yield
就可以触发promise_type中的yield_value
方法,来对promise object中的数值进行操作,而无需在协程中获取promise object即
co_yield value
等价于co_await promise_object.yield(value)
通过
co_yield
修改上述代码如下1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
#include <coroutine> #include <iostream> // 整体需求:实现协程,此协程无限生成递增的序列 // 协程不负责输出,只负责生成序列 // 输出的工作仅在主函数中执行 struct ReturnObject { struct promise_type { ReturnObject get_return_object() { return { .coroutine_handle = std::coroutine_handle<promise_type>::from_promise(*this) }; } std::suspend_never initial_suspend() { return {}; } std::suspend_never final_suspend() noexcept { return {}; } void unhandled_exception() {} std::suspend_always yield_value(unsigned int yield_value) { value = yield_value; return {}; } unsigned int value; }; std::coroutine_handle<promise_type> coroutine_handle; // 重载类型转换运算符operator type() {},将当前类型转化为协程句柄类型 operator std::coroutine_handle<promise_type>() { return coroutine_handle; } }; ReturnObject counter() { // 需要在协程获取promise对象 for (unsigned int i = 0;;i++) { co_yield i; } } int main() { std::coroutine_handle<ReturnObject::promise_type> h = counter(); for (unsigned int i = 0; i < 3; i++) { std::cout << "counter value: " << h.promise().value << std::endl; h(); } h.destroy(); }
co_return
co_return用于结束协程时执行
co_return;
等价于调用promise_object.return_void();
co_return value;
等价于调用promise_object.return_value(value);
对于上述代码,如果不想在主函数(即协程的调用者)一方决定何时结束协程,而由协程自己决定,则可以使用
co_return
来实现,而协程的调用者只需通过h.done()
来判断协程是否结束显示调用
co_return;
时,如果promise_type
中没有实现return_void
会给出提示,即编译不会通过在协程的末尾会默认调用
co_return
,但是如果在promise_type
中没有定义return_void
,则会产生ub(undefined behavior)因此,最好在
promise_type
中给出return_void
的实现修改后的代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
#include <coroutine> #include <iostream> // 整体需求:实现协程,此协程无限生成递增的序列 // 协程不负责在输出,只负责生成序列 // 输出的工作仅在主函数中执行 struct ReturnObject { struct promise_type { ReturnObject get_return_object() { return { .coroutine_handle = std::coroutine_handle<promise_type>::from_promise(*this)}; } std::suspend_never initial_suspend() { return {}; } std::suspend_never final_suspend() noexcept { return {}; } void unhandled_exception() {} std::suspend_always yield_value(unsigned int yield_value) { value = yield_value; return {}; } void return_void() {} unsigned int value; }; std::coroutine_handle<promise_type> coroutine_handle; // 重载类型转换运算符operator type() {},将当前类型转化为协程句柄类型 operator std::coroutine_handle<promise_type>() { return coroutine_handle; } }; ReturnObject counter() { // 需要在协程获取promise对象 for (unsigned int i = 0; i < 3; i++) { co_yield i; } co_return; // 这句话可以省略 } int main() { std::coroutine_handle<ReturnObject::promise_type> h = counter(); while (!h.done()) { std::cout << "counter value: " << h.promise().value << std::endl; h(); } h.destroy(); }
对于co_return后协程是否被销毁,是由promise_type的final_suspend决定的,如果final_suspend()返回
std::suspend_always
则协程不会被销毁,返回std::suspend_never
则协程会销毁,一个协程内部的执行流程如下述伪代码所述1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
{ promise-type promise promise-constructor-arguments ;//调用promise_type的构造函数 try { co_await promise.initial_suspend() ; function-body // 协程的函数体 } catch ( ... ) { if (!initial-await-resume-called) throw ; promise.unhandled_exception() ; } final-suspend : co_await promise.final_suspend() ; } // "The coroutine state is destroyed when control flows // off the end of the coroutine"