C++20 协程实战:从生成器到异步 IO

C++20 协程实战:从生成器到异步 IO

在上一篇文章中,我们了解了协程的基本概念。今天我们将不再纸上谈兵,而是深入到底层,编写三个具有实际意义的协程组件。

我们将涵盖:

  1. 泛型生成器 (Generator<T>):像 Python 那样产生无限序列。
  2. 自定义等待体 (Awaiter):理解 co_await 到底在等什么?
  3. 异步任务 (Task<T>):如何实现协程之间的互相调用。

场景一:泛型生成器 (Generic Generator)

上一篇我们写死了一个 int 生成器。在实际开发中,我们通常需要一个通用的模板类,可以生成 intstring 甚至复杂的结构体。这也是最典型的“懒加载”应用场景。

代码实现

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
85
#include <coroutine>
#include <iostream>
#include <optional>

// 1. 定义成模板类
template<typename T>
struct Generator {
struct promise_type;
using handle_type = std::coroutine_handle<promise_type>;

struct promise_type {
// 使用 std::optional 存储可能为空的值
std::optional<T> current_value;

Generator get_return_object() {
return Generator{handle_type::from_promise(*this)};
}

std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }

// 泛型 yield_value
std::suspend_always yield_value(T value) {
current_value = value;
return {};
}

void return_void() {}
void unhandled_exception() { std::terminate(); }
};

handle_type h;

Generator(handle_type h) : h(h) {}
~Generator() { if (h) h.destroy(); }
Generator(const Generator&) = delete;
Generator(Generator&& other) noexcept : h(other.h) { other.h = nullptr; }

// 迭代器接口:为了支持 range-based for loop (for(auto x : gen))
struct Iterator {
handle_type h;

bool operator!=(std::default_sentinel_t) const {
return !h.done();
}

void operator++() {
h.resume();
}

T operator*() const {
return *h.promise().current_value;
}
};

// begin 触发第一次执行
Iterator begin() {
if (h) h.resume();
return Iterator{h};
}

std::default_sentinel_t end() { return {}; }
};

// --- 使用案例:斐波那契数列 ---

Generator<uint64_t> fibonacci(int max_count) {
uint64_t a = 0, b = 1;
for (int i = 0; i < max_count; ++i) {
co_yield a;
auto temp = a;
a = b;
b = temp + b;
}
}

int main() {
std::cout << "Fibonacci sequence:" << std::endl;

// 像遍历容器一样遍历协程
for (auto num : fibonacci(10)) {
std::cout << num << " ";
}
std::cout << "\nDone." << std::endl;
}

要点解析

  1. 模板化promise_type 中的 current_value 改为 T 类型。
  2. 迭代器支持:我们添加了 begin()end() 以及内部类 Iterator。这使得我们可以直接使用 C++ 的 for (auto x : gen) 语法,这才是现代 C++ 的味道。

场景二:理解 co_await 与自定义 Awaiter

这是 C++ 协程最难也最强大的部分。
当你写下 co_await X; 时,编译器其实是在问对象 X:“我需要挂起吗?如果挂起,我该把控制权交给谁?

我们通过实现一个非阻塞的“休眠”功能来演示。普通的 std::this_thread::sleep_for 会卡死线程,而我们的 AsyncSleep 只会挂起协程,线程可以去干别的事。

代码实现

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
#include <coroutine>
#include <iostream>
#include <thread>
#include <chrono>

// 自定义 Awaiter 对象
struct AsyncSleeper {
int _ms;

// 1. await_ready: 是否准备好了?
// 返回 false 表示“还没准备好,请挂起协程”
// 返回 true 表示“无需挂起,直接继续”
bool await_ready() const { return false; }

// 2. await_suspend: 挂起后做什么?
// h 是当前协程的句柄。
// 在这里,我们将任务交给一个后台线程,模拟异步 I/O
void await_suspend(std::coroutine_handle<> h) {
// 启动一个新线程来倒计时,结束后恢复协程
std::thread([h, this]() {
std::this_thread::sleep_for(std::chrono::milliseconds(_ms));
std::cout << " [Background] Timer done, resuming coroutine...\n";
h.resume(); // 关键!在后台线程中恢复协程
}).detach();
}

// 3. await_resume: 恢复后返回什么?
// 这里的返回值就是 co_await 表达式的结果
void await_resume() {}
};

// 封装成函数
AsyncSleeper async_sleep(int ms) {
return AsyncSleeper{ms};
}

// 定义一个最简单的 Task 类型用于承载协程
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; } // 立即执行
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};

Task testAsync() {
std::cout << "[Coroutine] Start sleeping..." << std::endl;

// 切出当前线程,挂起协程
co_await async_sleep(1000);

std::cout << "[Coroutine] Resumed and finished!" << std::endl;
}

int main() {
std::cout << "[Main] Call coroutine" << std::endl;
testAsync();

std::cout << "[Main] Coroutine suspended, main thread continues work..." << std::endl;

// 为了演示效果,主线程等待一下子,否则主线程结束进程就退出了
std::this_thread::sleep_for(std::chrono::milliseconds(1500));
std::cout << "[Main] Exit" << std::endl;
}

运行结果

1
2
3
4
5
6
[Main] Call coroutine
[Coroutine] Start sleeping...
[Main] Coroutine suspended, main thread continues work...
[Background] Timer done, resuming coroutine...
[Coroutine] Resumed and finished!
[Main] Exit

要点解析

这个例子展示了协程真正的威力:异步非阻塞

  • await_suspend 是魔法发生的地方。我们在这里把控制权转移给了后台线程。
  • main 线程在协程挂起后,立即拿回了控制权,继续打印日志,没有被卡住。

场景三:协程组合 (Task waiting Task)

在真实业务中,我们很少只用一个协程,通常是协程调用协程(例如:HTTP 请求调用数据库查询,数据库查询调用 Socket 读取)。

为了实现 co_await OtherCoroutine();,协程的返回值类型(Task)本身必须也是一个 Awaiter

代码实现

这是一个简化的 Task<T> 实现,支持嵌套调用。

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
#include <coroutine>
#include <iostream>
#include <exception>

template<typename T>
struct Task {
struct promise_type;
using handle_type = std::coroutine_handle<promise_type>;
handle_type h;

Task(handle_type h) : h(h) {}
~Task() { if (h) h.destroy(); }

// --- 让 Task 变成 Awaitable 的关键 ---
// 1. 不立即 ready,必须挂起等待子协程跑完
bool await_ready() { return false; }

// 2. 挂起时,记录父协程的句柄,以便子协程跑完后恢复父协程
void await_suspend(std::coroutine_handle<> caller) {
h.promise().previous = caller; // 保存父协程
h.resume(); // 启动子协程
}

// 3. 子协程跑完了,取出结果
T await_resume() {
return h.promise().result;
}

struct promise_type {
T result;
std::coroutine_handle<> previous; // 记录谁调用了我

Task get_return_object() {
return Task{handle_type::from_promise(*this)};
}

// 创建时挂起,等待 await_suspend 里手动 resume
std::suspend_always initial_suspend() { return {}; }

// 结束后挂起,为了让父协程读取结果,并手动恢复父协程
struct final_awaiter {
bool await_ready() const noexcept { return false; }
void await_suspend(handle_type h) noexcept {
// 如果有父协程,恢复它
if (auto prev = h.promise().previous) {
prev.resume();
}
}
void await_resume() noexcept {}
};

final_awaiter final_suspend() noexcept { return {}; }

void return_value(T value) { result = value; }
void unhandled_exception() { std::terminate(); }
};
};

// --- 嵌套调用测试 ---

Task<int> add(int a, int b) {
std::cout << " -> inner: calculating " << a << " + " << b << std::endl;
co_return a + b;
}

Task<int> calculate_sum() {
std::cout << "-> outer: calling add" << std::endl;
// 协程等待另一个协程
int ret = co_await add(10, 20);
std::cout << "-> outer: got result " << ret << std::endl;
co_return ret * 2;
}

int main() {
auto t = calculate_sum();

// 手动启动最外层协程(因为我们设了 initial_suspend)
// 在成熟的库中,通常会有一个 sync_wait 来启动
t.h.resume();

std::cout << "Final result in main: " << t.h.promise().result << std::endl;
return 0;
}

要点解析

  1. 对称传输:协程 A 等待协程 B,A 必须把自己的句柄传给 B(存在 B.promise().previous 里)。
  2. 链式恢复:当 B 结束时(final_suspend),它检查 previous,发现是 A,于是调用 A.resume()。A 醒来后通过 await_resume 拿到 B 的结果。
  3. 组合性:这就是现代异步编程的核心——你可以像写同步代码一样,把复杂的异步逻辑层层封装。

总结

通过这三个例子,你应该能感受到 C++20 协程的各个层面:

  1. Generator:利用 co_yield 做懒加载迭代器。
  2. Awaiter:利用 co_awaitawait_suspend 对接异步系统(线程、IO)。
  3. Task:利用 promise_type 存储父级句柄,实现协程的嵌套组合。

给读者的建议
如果你的博客读者主要是应用层开发者,建议重点介绍 GeneratorAsyncSleep 的概念,因为 Task 的实现细节过于底层。你可以告诉他们:“在 C++23 或使用 folly/cppcoro 等库时,你们只需要写 co_await,而不需要写这些 promise_type。”


C++20 协程实战:从生成器到异步 IO
https://www.psnow.sbs/2025/12/29/C-20-协程实战:从生成器到异步-IO/
作者
Psnow
发布于
2025年12月30日
许可协议