接前面Python 协程小记与JavaScript异步函数,回头在看看C++中的协程。
C++2020引入协程,C++2023引入std::generator
,通过例子,学习一下。
C++ 生成器
C++2023 引入std::generator
,所以支持下面这种写法(尽管截至目前只有GCC libstdc++ 支持。MSVC在experimental中):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | #include <generator>
#include <iostream>
std::generator<int> my_generator() {
co_yield 1;
co_yield 2;
co_yield 3;
}
int main() {
for (int value : my_generator()) {
std::cout << value << std::endl;
}
return 0;
}
// 1
// 2
// 3
|
和同时期的其他语言比,C++中的generator晚了好多年。
其他语言
- python 支持生成器(python 2.2,2001年):
| def my_generator():
yield 1
yield 2
yield 3
for value in my_generator():
print(value)
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | using System;
public static class Program
{
public static System.Collections.Generic.IEnumerable<int> Generate()
{
yield return 1;
yield return 2;
yield return 3;
}
public static void Main()
{
foreach (var value in Generate())
{
Console.WriteLine(value);
}
}
}
|
或
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 | using System;
using System.Collections;
public class MyGenerator // : IEnumerable
{
public IEnumerator GetEnumerator()
{
yield return 1;
yield return 2;
yield return 3;
}
}
class Program
{
static void Main(string[] args)
{
foreach (var value in new MyGenerator())
{
Console.WriteLine(value);
}
}
}
|
| function* myGenerator() {
yield 1;
yield 2;
yield 3;
}
for (const value of myGenerator()) {
console.log(value);
}
|
协程
std::generator
是 C++2023才引入的。截至目前(2024年3月),主流编译器也只支持C++2020的大部分特性。
借用一张图:
A调用B,但是B又恢复A的执行。就像一个多次进入和退出的函数,和普通函数(function)、子程序(subroutine)相比,还是挺神奇的。
协程函数?
一个函数只要包含下面任何关键字,就是协程
co_await
:暂停协程执行,直到等待的操作(另一个协程或异步操作)完成。
co_yield
:暂停协程执行并向调用方返回一个值(协程恢复时从该位置继续执行),用于按顺序生成一系列值。
co_return
:从协程返回值并标志其完成(提前退出)。
这几个关键字看起来挺简单的,co_yield
、co_await
、co_return
主要配合完成交出控制权,但是协程函数的返回值(比如前面的std::generator
)有特定的要求,显得很复杂。协程暂停后,需要靠它来恢复协程。
还是先看例子
生成器例子
在C++2020中,提供了coroutine的基础设施,但没有具体的协程类。
要实现上面类似生成器的功能,需要自行定义一个MyGenerator类,而后将其作为协程函数的返回值才能用:
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
79
80
81
82
83
84 | #include <coroutine>
#include <iostream>
template<typename T>
struct MyGenerator {
struct promise_type {
T current_value;
MyGenerator get_return_object() {
return MyGenerator{ std::coroutine_handle<promise_type>::from_promise(*this) };
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(T value) {
current_value = value;
return {};
}
void return_void() {}
void unhandled_exception() {
std::terminate();
}
};
std::coroutine_handle<promise_type> coroutine;
explicit MyGenerator(std::coroutine_handle<promise_type> coro) : coroutine(coro) {}
~MyGenerator() {
if (coroutine) {
coroutine.destroy();
}
}
T current_value() const {
return coroutine.promise().current_value;
}
struct iterator {
MyGenerator& gen;
bool operator!=(const iterator&) const {
return gen.coroutine && !gen.coroutine.done();
}
const T& operator*() const {
return gen.current_value();
}
void operator++() {
if (gen.coroutine) {
gen.coroutine.resume();
}
}
};
iterator begin() {
if (coroutine) {
coroutine.resume();
}
return { *this };
}
iterator end() {
return { *this };
}
};
MyGenerator<int> my_generator() {
co_yield 1;
co_yield 2;
co_yield 3;
}
int main() {
for (int value : my_generator()) {
std::cout << value << std::endl;
}
return 0;
}
|
C++2020的这个东西,就不像是给普通用户用的。需要先封装成库才行。上面为了用range for,定义了迭代器。
即使去掉range for支持,代码还是挺乱:
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 T>
struct MyGenerator {
struct promise_type {
T current_value;
MyGenerator get_return_object() {
return MyGenerator{ std::coroutine_handle<promise_type>::from_promise(*this) };
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(T value) {
current_value = value;
return {};
}
void return_void() {}
void unhandled_exception() {
std::terminate();
}
};
std::coroutine_handle<promise_type> coroutine;
explicit MyGenerator(std::coroutine_handle<promise_type> coro) : coroutine(coro) {}
~MyGenerator() {
if (coroutine) {
coroutine.destroy();
}
}
bool move_next() {
if (!coroutine.done()) {
coroutine.resume();
return !coroutine.done(); // Check if the coroutine is done after resuming
}
else {
return false;
}
}
T current_value() const {
return coroutine.promise().current_value;
}
};
MyGenerator<int> my_generator() {
co_yield 1;
co_yield 2;
co_yield 3;
}
int main() {
MyGenerator<int> gen = my_generator();
while (gen.move_next())
{
std::cout << gen.current_value() << std::endl;
}
return 0;
}
|
涉及到的类有点多,先放个图:
拆开看看?promise_type
C++协程中的promise概念 和 std::promise 以及 std::future 完全不是一个东西。
要定义协程函数,先需要定义一个结构体作为协程函数的返回值类型,该结构体需要有一个promise_type
结构体成员
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 | #include <iostream>
#include <coroutine>
struct MyGenerator {
struct promise_type {
std::suspend_never initial_suspend() { std::cout << "initial_suspend()\n"; return {}; }
std::suspend_always final_suspend() noexcept { std::cout << "final_suspend()\n"; return {}; }
std::suspend_never yield_value(int value) {std::cout << "yield_value() "<<value<<"\n"; return {}; }
void return_void() { std::cout << "return_void()\n"; }
MyGenerator get_return_object() {std::cout << "get_return_object()\n"; return {}; }
void unhandled_exception() {std::terminate(); }
};
};
MyGenerator simpleCoroutine() {
co_yield 1;
co_yield 2;
co_yield 3;
}
int main() {
std::cout << "before coro" << std::endl;
auto coro = simpleCoroutine();
std::cout << "after coro" << std::endl;
return 0;
}
// before coro
// get_return_object()
// initial_suspend()
// yield_value() 1
// yield_value() 2
// yield_value() 3
// return_void()
// final_suspend()
// after coro
|
promise_type` 结构体定义了协程的 Promise 接口,它包含了协程执行的状态和用于处理异步等待的函数。Promise 接口有以下成员函数:
get_return_object()
: 返回用于保存协程状态的对象。
initial_suspend()
: 返回协程开始时 暂停状态的 awaitable 对象。用于决定协程开始执行时,是否立即暂停。
final_suspend()
: 返回协程结束时 暂停状态的 awaitable 对象。用于决定协程结束执行时,是否立即暂停。
yield_value()
: 接受co_yield
值并返回中途交出控制权时暂停状态的 awaitable 对象。
return_void()
: 描述协程返回void时的行为(没有co_return
或者co_return;
)。协程执行完毕,且没有中途交出控制权的情况下,被调用。
return_value()
:描述协程返回非void类型时的行为(使用语句 co_return v;
)。协程执行完毕,且没有中途交出控制权的情况下,被调用。
unhandled_exception()
: 处理未处理的异常。协程内发生异常且没有被catch时,函数被调用。
上面例子中,刻意将initial_suspend()
和yield_value()
返回值都设置为std::suspend_never
,使得协程初始化和遇到co_yield
时都不暂停,直接执行到底。不然在这个例子中,一旦暂停后,没有办法恢复它。final_suspend
的返回值std::suspend_always
还是其他都可以,取觉于要不要这时候使用协程资源。
协程函数调用promise的成员,如果协程函数展开,可以看作下面这样的伪代码:
1
2
3
4
5
6
7
8
9
10
11
12
13 | {
promise-type promise promise-constructor-arguments ;
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() ;
}
|
co_await
:最基础
co_yield v;
:可以想象成 co_await promise.yield_value(v);
co_return;
和 co_return v;
:分别对应 promise.return_void()
与promise.return_value(v)
。但是它们都不返回Awaitable,也就是不能用暂停协程。
拆开看看?std::coroutine_handle
promise_type
是对内的,对外,需要std::coroutine_handle
。这个结构体还挺复杂,一个promise的特化,一个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 | template <class = void>
struct coroutine_handle;
template <>
struct coroutine_handle<void> {
//...
bool done() const {
return __builtin_coro_done(_Ptr);
}
void operator()() const {
__builtin_coro_resume(_Ptr);
}
void resume() const {
__builtin_coro_resume(_Ptr);
}
void destroy() const {
__builtin_coro_destroy(_Ptr);
}
private:
void* _Ptr = nullptr;
};
template <class _Promise>
struct coroutine_handle {
static coroutine_handle from_promise(_Promise& _Prom) {
const auto _Prom_ptr = const_cast<void*>(static_cast<const volatile void*>(_STD addressof(_Prom)));
const auto _Frame_ptr = __builtin_coro_promise(_Prom_ptr, 0, true);
coroutine_handle _Result;
_Result._Ptr = _Frame_ptr;
return _Result;
}
constexpr operator coroutine_handle<>() const {
return coroutine_handle<>::from_address(_Ptr);
}
//....
_Promise& promise() const {
return *reinterpret_cast<_Promise*>(__builtin_coro_promise(_Ptr, 0, false));
}
private:
void* _Ptr = nullptr;
}
|
协程暂停之后,需要调用coroutine_handle
的resume()
才能恢复。
这个coroutine_handle
是在promise_type
中的get_return_object()
中创建的:
| MyGenerator get_return_object() {
return MyGenerator{ std::coroutine_handle<promise_type>::from_promise(*this) };
}
|
- 协程函数被调用时,通过
get_return_object()
获取 MyGenerator
类型的对象,并返回该 MyGenerator
对象。
MyGenerator
构造时会创建coroutine_handle
对象,并作为结构体成员存储。
coroutine_handle
的析构函数并不会释放资源,必须调用destroy()
。
注意模板的使用,先声明了模板默认参数类型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 | #include <iostream>
template <class = void>
struct S;
template <>
struct S<void>
{
int v = 1;
};
template<class T>
struct S
{
T v = 2;
operator S<>() const {
return S<>{.v = static_int<int>(v) };
}
};
int main() {
S a0;
S<double> a1;
S a2 = a1;
std::cout << a0.v << " " << a1.v << " " << a2.v << std::endl; // 1 2 2
return 0;
}
|
拆开看看? Awaitable
标准库中的suspend_never
和suspend_always
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | struct suspend_never {
constexpr bool await_ready() const {
return true;
}
constexpr void await_suspend(coroutine_handle<>) const {}
constexpr void await_resume() const {}
};
struct suspend_always {
constexpr bool await_ready() const {
return false;
}
constexpr void await_suspend(coroutine_handle<>) const {}
constexpr void await_resume() const {}
};
|
它们实现了特定的 awaitable 接口:
await_ready()
: 返回一个布尔值,指示等待是否立即完成。如果等待立即完成,返回 true
;否则返回 false
。
await_suspend()
: 描述协程在等待时的暂停状态。返回一个 std::coroutine_handle
或类似对象,指示协程的挂起点。这个函数通常会保存等待时的状态,以便在等待完成后恢复协程的执行。
await_resume()
: 返回等待完成后的结果。当等待完成时,此函数用于获取最终结果。
用自定义类取代std::suspend_always
:
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>
struct MyAwaitable
{
bool await_ready() const noexcept {
return false;
}
void await_suspend(std::coroutine_handle<>) const noexcept { std::cout << "await_suspend()\n"; }
void await_resume() const noexcept { std::cout << "await_resume()\n"; }
};
template<typename T>
struct MyGenerator {
struct promise_type {
T current_value;
MyGenerator get_return_object() {
return MyGenerator{ std::coroutine_handle<promise_type>::from_promise(*this) };
}
MyAwaitable initial_suspend() { return {}; }
MyAwaitable final_suspend() noexcept { return {}; }
MyAwaitable yield_value(T value) {
current_value = value;
return {};
}
void return_void() {}
void unhandled_exception() {
std::terminate();
}
};
std::coroutine_handle<promise_type> coroutine;
explicit MyGenerator(std::coroutine_handle<promise_type> coro) : coroutine(coro) {}
~MyGenerator() {
if (coroutine) {
coroutine.destroy();
}
}
bool move_next() {
if (!coroutine.done()) {
coroutine.resume();
return !coroutine.done(); // Check if the coroutine is done after resuming
}
else {
return false;
}
}
T current_value() const {
return coroutine.promise().current_value;
}
};
MyGenerator<int> my_generator() {
co_yield 1;
co_yield 2;
co_yield 3;
co_return;
}
int main() {
MyGenerator<int> gen = my_generator();
while (gen.move_next())
{
std::cout << gen.current_value() << std::endl;
}
return 0;
}
|
结果:
1
2
3
4
5
6
7
8
9
10
11
12 | await_suspend()
await_resume()
await_suspend()
1
await_resume()
await_suspend()
2
await_resume()
await_suspend()
3
await_resume()
await_suspend()
|
在这个例子中,由于 final_suspend()
返回值不是std::suspend_never
,所以final_suspend
后我们还可以访问协程信息。如下:
| int main() {
MyGenerator<int> gen = my_generator();
while (gen.move_next())
{
std::cout << gen.current_value() << std::endl;
}
std::cout << gen.coroutine.done() << std::endl;
return 0;
}
|
如果 返回值是std::suspend_never
,协程到时会被自动销毁,再尝试方位协程信息会崩溃。
另外,Awaitable用在co_await
后面,所以前面的例子,甚至都可以这么写:
1
2
3
4
5
6
7
8
9
10
11
12
13 | MyGenerator<int> my_generator() {
co_await MyAwaitable();
co_await MyAwaitable();
co_await MyAwaitable();
co_await MyAwaitable();
co_await MyAwaitable();
co_await MyAwaitable();
co_yield 1;
co_yield 2;
co_yield 3;
co_return;
}
|
编译环境
VS2019 与 C++20
尽管 VS2019 支持 C++20,使用VS2019的IDE的话,需要手动选择C++语言标准为 /std:c++20
或/std:c++latest
。另外,确保构建是x64
而不是x86
或win32。不然都会有类似下面的错误
| namespace std contains no member suspend_always
|
在VS2019下,generator在experimental中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | #include <experimental/generator>
#include <iostream>
std::experimental::generator<int> my_generator() {
co_yield 1;
co_yield 2;
co_yield 3;
}
int main() {
for (int value : my_generator()) {
std::cout << value << std::endl;
}
return 0;
}
|
GCC 协程选项
| g++ -std=c++20 -fcoroutines my_coro.cpp
g++ -std=c++20 my_coro.cpp
|
其中,在GCC10中,需要通过-fcoroutines
用于启用协程支持 (experimental)。GCC11起,不再需要这个选项。
参考
- https://en.cppreference.com/w/cpp/links
- https://en.cppreference.com/w/cpp/compiler_support/20
- https://gcc.gnu.org/projects/cxx-status.html
- https://en.cppreference.com/w/cpp/language/coroutines
- https://en.cppreference.com/w/cpp/coroutine
- https://www.scs.stanford.edu/~dm/blog/c++-coroutines.html#compiling-code-using-coroutines
- https://lewissbaker.github.io/2017/09/25/coroutine-theory
- https://itnext.io/c-20-coroutines-complete-guide-7c3fc08db89d
- https://www.scs.stanford.edu/~dm/blog/c++-coroutines.pdf