C++20迈向异步 Part2-可等待体

C++20迈向异步 Part1-最简单的协程

时隔大半年,经历了老博客挂掉/Gnome换KDE等风波之后,总算迎来了计算机网络。也是读了一遍《Unix 网络编程 卷一:套接字联网API》和《UNIX 环境高级编程》,对异步和网络有了全新的认识。是时候重启这个系列了,而本Part将会介绍co_await和可等待体,并给出上一Part中generator的改进版,我们的协程现在可以等待另一个协程了。

可等待体

如果说从std::coroutine_traits::promise_type获得的用户定义的承诺对象使用std::coroutine_handle控制着整个协程的话,那么从承诺对象获得的可等待体则控制着co_await原语的行为:暂停前该干什么、是否要暂停、暂停后该干什么。为此C++为可等待体类型设计了三个鸭子函数:

怎么一股子原始C命名风格味

1
2
3
T await_ready() noexcept;
void/bool/coroutine_handle await_suspend(coroutine_handle<>) noexcept;
void await_resume() noexcept;

await_ready指示此等待体是否无需等待,bool(T)返回false表示需要等待。

在暂停协程后将协程句柄作为参数调用await_suspend,如果返回void或者false或者NULL则返回调用方,返回true则恢复协程。返回coroutine_handle则恢复这个协程。注意可等待体的生命周期严格等于协程的暂停-恢复,所以如果在await_suspend里有可能恢复本协程的执行,那么就应该假定*thiscoroutine_handle已经被销毁,此时await_resume可能会先于await_suspend结束而执行。

当协程恢复执行前调用await_resume,其返回值就是await表达式的返回值。

STL中定义的可等待体

这些可等待体代表了一些基本的行为

1
2
class suspend_never{};
class suspend_always{};

它们的await_suspendawait_resume都返回void,而await_ready前者返回true后者返回false。所以你可以直接co_await它们。

1
2
3
4
5
6
7
/*接Part1代码*/
coroutine_handle<promise> fibonacci(int a=1){
co_await suspend_never{};
co_await suspend_never{};
co_await suspend_never{};
/*...*/
}

完全没作用。

1
2
3
4
5
6
7
/*接Part1代码*/
coroutine_handle<promise> fibonacci(int a=1){
co_await suspend_never{};
co_await suspend_never{};
co_await suspend_never{};
/*...*/
}

可以发现结果起码会出现3个0 0(随机交错执行,具体可能不一样)。

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

这就意味着co_await suspend_never使协程暂停了三次,三次返回调用方,而调用方还是main函数里的交错执行循环。

重新审视promise和coroutine_handle

我们可以发现可等待体的定义完全可以脱离协程而存在,而可等待体的使用则和协程密不可分。让我们回头来看Part1的代码:

在Part1中的讲述协程具体实现的代码中有两处用到了co_await

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
/*
协程的创建
*/
promise-type promise promise-constructor-arguments ;
/*
尝试将函数参数传入到promise构造函数。如果失败则使用默认构造函数。
*/
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() ;
/*
销毁promise
销毁栈帧
销毁协程状态
*/
}

可以发现其决定协程刚开始执行就会被暂停和协程结束执行会不会暂停。

当协程被暂停时,此时协程的上下文信息保存在堆中,coroutine_handle就指示这个堆对象。

coroutine_handle::destory()可以销毁堆上的协程栈帧,会依次序执行上述销毁语句(和自动变量)。

coroutine_handle::d()

generator最终版

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
68
69
70
71
72
73
74
75
76
77
78
#include <coroutine>
#include <iostream>
#include <optional>
#include <tuple>

using namespace std;

template <class T>
struct promise;

template <class T>
struct generator;

namespace std {
template <class T, class... Args>
struct coroutine_traits<generator<T>, Args...> {
using promise_type = promise<T>;
};
}

template <class T>
struct promise {
promise():done(false){}
auto initial_suspend() { return suspend_always{}; }
auto final_suspend() noexcept { return suspend_always{}; }
void unhandled_exception() { terminate(); }

generator<T> get_return_object() { return {this}; }
void return_void() {
done = true;
}
template <class U>
auto yield_value(U&& value) {
value_ = move(forward<U>(value));
return suspend_always{};
}
bool done;
T value_;
};

template <class T>
struct generator {
promise<T>* f;
~generator() {
if (f)
handle().destroy();
}
coroutine_handle<promise<T>> handle() {
return coroutine_handle<promise<T>>::from_promise(*f);
}
operator bool() { return f; }
T* operator()() {
if (!f) return nullptr;
handle()();
if (f->done){
handle().destroy();
f = nullptr;
return nullptr;
}
return &f->value_;
}
};

generator<int> fibonacci(int a = 1) {
int i = 0, j = 1;
while (j<=100) {
co_yield j;
tie(i, j) = make_pair(j, i + j);
}
}

int main(int argc, char** argv) {
auto x = fibonacci();
while (auto y=x()) {
cout << *y << endl;
}
return 0;
}

当然跟python一样,C++23的generator将会是ranges::view。这就意味着可以使用迭代器的方法使用generator,就像istream_iterator一样。

在下一节,我们将会使用socket套接字API实现TCP/UDP通讯并整合进C++协程,使用select原语实现事件循环。建立起一个简单的异步IO库,并提一点关于asio的有趣信息。