概述
C++20协程的核心不是“创建线程”,而是- 把一个函数编译成可以暂停、保存现场、稍后恢复的状态机
- 协程本身不提供线程、调度器、事件循环或异步
I/O- 它只提供语言机制,真正决定“何时恢复”的是
Awaiter、线程池、Asio等运行时
- 它只提供语言机制,真正决定“何时恢复”的是
关键字
co_await
- 等待某个操作,必要时暂停协程
|
1 |
Result result = co_await expression; |
- 上面的操作大致经过三个阶段
|
1 2 3 |
awaiter.await_ready() awaiter.await_suspend(handle) awaiter.await_resume() |
|
1 2 3 4 5 6 7 8 |
struct Awaiter { bool await_ready(); void await_suspend( std::coroutine_handle<> handle); Result await_resume(); }; |
co_yield
- 产生一个值,然后暂停协程
co_yield适合逐个产生数据C++20标准库没有std::generatorC++23才引入它。C++20需要库提供或自己实现
|
1 2 3 4 5 6 |
Generator<int> sequence() { co_yield 1; co_yield 2; co_yield 3; } |
|
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 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
#include <coroutine> #include <exception> #include <iostream> #include <utility> template <typename T> class Generator { public: struct promise_type { T current_value{}; std::exception_ptr exception; Generator get_return_object() { return Generator{ Handle::from_promise(*this) }; } std::suspend_always initial_suspend() noexcept { return {}; } std::suspend_always final_suspend() noexcept { return {}; } std::suspend_always yield_value(T value) noexcept { current_value = std::move(value); return {}; } void return_void() noexcept {} void unhandled_exception() noexcept { exception = std::current_exception(); } using Handle = std::coroutine_handle<promise_type>; }; using Handle = std::coroutine_handle<promise_type>; explicit Generator(Handle handle) : handle_(handle) { } Generator(const Generator&) = delete; Generator& operator=(const Generator&) = delete; Generator(Generator&& other) noexcept : handle_(std::exchange(other.handle_, {})) { } ~Generator() { if (handle_) { handle_.destroy(); } } bool next() { if (!handle_ || handle_.done()) { return false; } handle_.resume(); if (handle_.promise().exception) { std::rethrow_exception( handle_.promise().exception); } return !handle_.done(); } const T& value() const { return handle_.promise().current_value; } private: Handle handle_; }; Generator<int> sequence(int count) { for (int i = 0; i < count; ++i) { co_yield i; } } int main() { Generator<int> generator = sequence(5); while (generator.next()) { std::cout << generator.value() << ' '; } } |
- 作用
- 技巧
co_return
- 结束协程,可以返回结果
- 协程不能用普通
return返回结果
- 协程不能用普通
|
1 2 3 4 5 6 |
co_return value; co_return; // 编译器会将其转换成 promise.return_value(value); promise.return_void(); |
其他
await_ready
|
1 |
bool await_ready(); |
- 返回
true
|
1 2 3 |
结果已经准备好 不暂停 直接调用 await_resume() |
- 返回
false
|
1 2 3 |
结果尚未准备好 保存协程现场 调用 await_suspend() |
await_suspend
|
1 2 |
void await_suspend( std::coroutine_handle<> handle); |
- 协程已经保存现场,
Awaiter获得协程句柄 - 它通常会:
- 把句柄加入线程池
- 注册到
epoll/IOCP - 交给定时器
- 保存到异步操作对象
- 在操作完成时调用
handle.resume()
|
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 |
// 线程池调度 Awaiter 可以写成 struct ScheduleAwaiter { ThreadPool& pool; bool await_ready() const noexcept { return false; } void await_suspend( std::coroutine_handle<> handle) { pool.enqueue([handle] { handle.resume(); }); } void await_resume() const noexcept {} }; // 协程中 Task<void> work(ThreadPool& pool) { std::cout << "当前线程执行\n"; co_await ScheduleAwaiter{pool}; std::cout << "在线程池线程恢复\n"; } |
await_resume
|
1 |
Result await_resume(); |
- 它负责
- 返回异步结果
- 检查错误
- 必要时抛出异常
|
1 2 3 4 5 6 7 8 |
int await_resume() { if (error_) { throw std::system_error(error_); } return bytes_transferred_; } |
promise_type 职责
promise_type不是std::promise。只是名字相似std::promise属于future体系- 协程的
promise_type是编译器与协程返回类型之间的协议
- 编译器通过返回类型找到
|
1 2 3 4 5 6 |
ReturnType::promise_type std::coroutine_traits< ReturnType, 参数类型... >::promise_type |
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 |
struct promise_type { // 创建调用者拿到的返回对象 ReturnType get_return_object(); // 协程体开始前是否暂停 Awaiter initial_suspend(); // 协程结束后是否暂停 Awaiter final_suspend() noexcept; // co_return value void return_value(T value); // co_return; void return_void(); // co_yield value Awaiter yield_value(T value); // 未捕获异常 void unhandled_exception(); // 可选:转换 co_await 表达式 Awaiter await_transform(...); }; |
实现原理:协程帧
概述
- 普通函数的局部变量通常放在调用线程栈上
- 协程可能在函数返回后继续存在,所以跨越暂停点的状态不能只放在普通调用栈中
- 编译器会创建协程帧:
|
1 2 3 4 5 6 7 8 9 10 |
Coroutine Frame ┌──────────────────────────┐ │ promise_type │ │ 函数参数副本 │ │ 跨越暂停点的局部变量 │ │ 当前执行状态编号 │ │ 异常信息 │ │ continuation │ │ 其他编译器状态 │ └──────────────────────────┘ |
- 协程帧通常分配在堆上,但标准允许编译器消除分配或嵌入调用者存储中
- 因此不能简单宣称“协程一定会进行堆分配”
编译器大致如何改写
- 原始协程
|
1 2 3 4 5 6 7 8 |
Task<int> calculate() { int value = 20; co_await operation(); co_return value + 22; } |
- 概念上会被改写为状态机
|
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 |
// 伪代码 struct CoroutineFrame { promise_type promise; int state; int value; Awaiter awaiter; }; void resume(CoroutineFrame* frame) { switch (frame->state) { case 0: frame->value = 20; frame->awaiter = operation(); if (!frame->awaiter.await_ready()) { frame->state = 1; frame->awaiter.await_suspend( coroutine_handle(frame)); return; } [[fallthrough]]; case 1: frame->awaiter.await_resume(); frame->promise.return_value( frame->value + 22); frame->state = 2; // 执行 final_suspend return; } } |
- 核心本质是
|
1 2 3 |
局部变量 → 协程帧字段 暂停位置 → 状态编号 恢复操作 → switch 跳转 |
生命周期问题
引用参数悬空
- 如果协程暂停期间
text被销毁,恢复后就会访问悬空引用- 协程帧保存的是引用,不会自动复制引用所指向的对象
|
1 2 3 4 5 |
Task<void> process(std::string& text) { co_await something(); std::cout << text; } |
- 必要时按值传递
协程 lambda 捕获悬空
- 如果
lambda闭包在协程恢复前销毁,协程可能通过悬空的this访问捕获成员
|
1 2 3 4 5 6 7 8 |
auto lambda = [data = std::string("hello")]() -> Task<void> { co_await something(); std::cout << data; }; Task<void> task = lambda(); |
- 更稳妥的方式是把数据作为协程函数参数传入,并确保进入协程帧
|
1 2 3 4 5 |
Task<void> process(std::string data) { co_await something(); std::cout << data; } |
协程帧必须准确销毁一次
- 会销毁
promise- 协程帧中的局部变量
- 参数副本
- 协程帧存储
|
1 |
handle.destroy(); |
- 不能
- 重复
destroy() - 销毁后再
resume() - 对已经到达最终暂停点的协程再次
resume() - 协程仍可能被异步回调恢复时提前销毁它
- 重复
std::coroutine_handle本身不会自动管理生命周期,通常需要Task或Generator用RAII包装它
不要并发恢复同一个协程
- 多个线程同时执行
- 通常会破坏协程帧状态,产生数据竞争或未定义行为
- 调度器必须保证一个协程同一时刻只被一个执行流恢复
|
1 |
handle.resume(); |
什么时候使用协程
适合
- 高并发网络连接
- 异步文件和数据库操作
- 定时器
GUI事件序列Generator- 状态机
- 多个异步操作的顺序组合
- 减少回调嵌套
不一定适合
- 简单同步计算
CPU密集任务但没有线程池- 极短且数量巨大的任务,协程帧成本不可忽略
- 库没有提供
Awaiter/调度器 - 生命周期很难证明
- 团队尚未建立统一的
Task、取消和错误协议
三个核心结论
C++20协程是编译器状态机转换机制,不是线程或调度器co_await是否暂停、在哪里恢复,完全由Awaiter协议决定- 真正困难的不是三个关键字,而是协程帧所有权、异步操作生命周期、取消、异常和调度器
最小可运行协程
code
C++20标准库没有直接提供通用的Task类型,所以需要自己定义返回对象
|
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 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
#include <coroutine> #include <iostream> #include <utility> class ManualTask { public: struct promise_type { ManualTask get_return_object() { return ManualTask{ std::coroutine_handle<promise_type>::from_promise(*this) }; } // 创建后立即执行协程体 std::suspend_never initial_suspend() noexcept { return {}; } // 执行完成后先暂停,等待外部销毁协程帧 std::suspend_always final_suspend() noexcept { return {}; } void return_void() noexcept {} void unhandled_exception() { std::terminate(); } }; using Handle = std::coroutine_handle<promise_type>; explicit ManualTask(Handle handle) : handle_(handle) { } ManualTask(const ManualTask&) = delete; ManualTask& operator=(const ManualTask&) = delete; ManualTask(ManualTask&& other) noexcept : handle_(std::exchange(other.handle_, {})) { } ManualTask& operator=(ManualTask&& other) noexcept { if (this != &other) { if (handle_) { handle_.destroy(); } handle_ = std::exchange(other.handle_, {}); } return *this; } ~ManualTask() { if (handle_) { handle_.destroy(); } } bool resume() { if (!handle_ || handle_.done()) { return false; } handle_.resume(); return !handle_.done(); } private: Handle handle_; }; ManualTask example() { std::cout << "协程开始\n"; co_await std::suspend_always{}; std::cout << "协程恢复\n"; co_return; } int main() { std::cout << "调用协程\n"; ManualTask task = example(); std::cout << "回到 main\n"; task.resume(); std::cout << "main 结束\n"; } |
|
1 2 3 4 5 |
调用协程 协程开始 回到 main 协程恢复 main 结束 |
执行过程
- 调用
- 并不是像普通函数一样从头执行到尾
|
1 |
ManualTask task = example(); |
- 执行过程是
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
创建协程帧 ↓ 构造 promise_type ↓ 调用 get_return_object() ↓ 调用 initial_suspend() ↓ 进入协程体 ↓ 执行到 co_await suspend_always ↓ 保存现场并返回 main ↓ 外部调用 task.resume() ↓ 从暂停位置继续执行 ↓ co_return ↓ final_suspend() ↓ ManualTask 析构时 destroy() |
suspend_always
- 表示一定暂停
suspend_never
- 表示不暂停
协程在网络编程中
Asio 回调
|
1 2 3 4 5 6 7 8 |
socket.async_read_some( asio::buffer(data), [this, self](std::error_code ec, std::size_t length) { if (!ec) { do_write(length); } }); |
Asio 的协程接口
- 可以写成更接近同步流程的代码
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
asio::awaitable<void> session(tcp::socket socket) { char data[1024]; for (;;) { std::size_t length = co_await socket.async_read_some( asio::buffer(data), asio::use_awaitable); co_await asio::async_write( socket, asio::buffer(data, length), asio::use_awaitable); } } |
|
1 2 3 4 |
asio::co_spawn( io_context, session(std::move(socket)), asio::detached); |
- 协程版本看起来像阻塞代码,但底层仍然是异步模型
|
1 2 3 4 5 6 7 8 9 10 11 |
调用 async_read ↓ 没有数据,注册异步读取 ↓ 暂停协程,线程返回 io_context ↓ socket 可读 ↓ io_context 恢复协程 ↓ 继续执行 async_write |
高级技巧
协程组合:Task 等待 Task
- 希望写出
- 外层协程等待内层协程
|
1 2 3 4 5 |
Task<int> request() { int value = co_await read_data(); co_return value * 2; } |
- 典型实现方式是
- 外层协程执行
co_await inner_task - 将外层句柄保存为内层的
continuation; - 启动或恢复内层协程
- 内层进入
final_suspend() - 内层恢复外层
continuation - 外层通过
await_resume()获取结果
- 外层协程执行
|
1 2 3 4 5 6 7 8 9 |
外层 Task A ↓ co_await B 保存 A 为 B.continuation ↓ 执行 Task B ↓ co_return B.final_suspend() ↓ 恢复 A |
对称转移
await_suspend()不一定返回void,还可以返回另一个协程句柄
|
1 2 3 4 5 |
std::coroutine_handle<> await_suspend(std::coroutine_handle<> current) { return next_coroutine; } |
- 运行时可以直接从当前协程转移到另一个协程
resume()的递归调用- 深层协程组合造成的调用栈增长
- 不必要的调度往返
异常传递
- 协程体中未捕获的异常不会直接随意逃出,而是调用
|
1 2 3 |
promise.unhandled_exception(); std::exception_ptr exception; |
- 在
await_resume()中重新抛出
|
1 2 3 4 |
if (promise.exception) { std::rethrow_exception( promise.exception); } |
取消
C++20协程本身没有自动取消功能
|
1 2 3 4 5 6 7 8 9 10 |
std::stop_token // 或异步库自己的取消机制 Task<void> work(std::stop_token token) { while (!token.stop_requested()) { co_await next_operation(); } } |
- 但仅检查
stop_token不一定能取消已经注册的异步I/O。真正取消通常还需要:- 从
epoll等待表移除 - 调用
CancelIoEx - 取消定时器
- 从线程池队列撤回任务
- 决定协程如何恢复并返回取消错误
- 从
调度线程切换
- 协程从哪个线程恢复,取决于谁调用
|
1 |
handle.resume(); |
|
1 2 3 4 |
线程 A 创建协程 线程 A 在 co_await 暂停 线程池的线程 B 调用 resume() 协程后半段在线程 B 执行 |
- 协程不具备线程亲和性。如果后续代码必须回到
UI线程或io_context线程,需要显式切换调度器
避免锁跨越暂停点
- 危险写法
|
1 2 3 |
std::lock_guard<std::mutex> lock(mutex); co_await async_operation(); |
- 协程暂停时,局部变量仍然存活,因此 mutex 会一直保持锁定,直到协程恢复并离开作用域
- 这可能造成:
- 长时间持锁
- 死锁
- 其他线程完全无法推进
- 异步操作等待的逻辑反过来需要这把锁
- 应当在暂停前释放锁:
|
1 2 3 4 5 6 |
{ std::lock_guard<std::mutex> lock(mutex); prepare_state(); } co_await async_operation(); |
声明:本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ C++20_第一篇06/30
- ♥ C++20_第三篇、协程一07/11
- ♥ C++_多线程相关03/12
- ♥ 51CTO:C++语言高级课程三08/15
- ♥ cpp-httplib:概述使用10/30
- ♥ STL_deque05/18