Table of contents
Open Table of contents
- TL;DR
- 1. Why:C++20 之前的 C++ 异步有多惨
- 2. 核心概念(大部分博客讲错或跳过的部分)
- 3. 编译器具体做了什么
- 4. 标准库给了什么,没给什么
- 5. C++20 可编译示例(GCC 13+ / Clang 17+)
- 6. 协程与异步 I/O 的结合(本项目重点)
- 7. 库作者应掌握的模式
- 8. Pitfalls(至少 12 条,每条都见过人踩)
- 9. 编译器与库支持
- 10. 生态库
- 11. 横向对比:协程 vs 线程 vs 回调 vs Goroutine vs Rust async
- 12. 关键提案与路线图
- 13. 生产 Checklist
- 参考(authoritative sources)
TL;DR
C++20 协程的本质是编译器把带 co_await/co_yield/co_return 的函数重写成状态机,把跨 suspend 存活的变量打包到一个堆上的 frame——这是语言级 stackless 协程,不是 Goroutine 式 stackful。标准库只给了 coroutine_handle / suspend_always / suspend_never 这几块地基,不给 Task<T>、不给 Generator(C++23 才加)、不给 executor、不给调度器。要在网络服务器里用,必须自己实现 Task<T>(严格遵守 symmetric transfer 否则链式 co_await 直接栈溢出)+ IoUringAwaiter(绑定到 reactor)。三大杀手级陷阱:协程参数必须按值传(引用参数跨 suspend 必悬挂)、final_suspend 必须 suspend_always(否则 double-free)、热路径不能依赖 HALO,需要自定义 operator new。
一句警示:C++20 协程看起来像 Go 的 goroutine,实际上它不是运行时特性而是语言特性——没有调度器、没有取消、没有线程池,裸 C++20 写异步服务器比写 epoll reactor 更容易踩坑。cppcoro / Asio awaitable / libunifex / C++26
std::execution是四条合理外挂路线。
1. Why:C++20 之前的 C++ 异步有多惨
C++20 之前写高性能异步 I/O 只有四条路,每条在某个维度上破产:
| 方案 | 破产点 |
|---|---|
| 回调 reactor(epoll + lambda 链) | 一次逻辑请求散落到十几个 handler;生命周期靠 shared_ptr<Session> + enable_shared_from_this;异常无法穿透,只能透传 error_code;栈回溯跨不过回调边界 |
| 手写状态机(Protothreads、旧 Asio stackless) | switch/case + duff's device,局部变量必须提升到类成员,嵌套调用不可行,可读性灾难 |
| 有栈协程(Boost.Context / Fiber、libco、bthread) | 每 fiber 一个栈,10w 并发 × 64KB = 6.4GB 地址空间;ABI 敏感(汇编 swapcontext);ASan/TSan 支持不完整 |
| 线程(1:1 threading) | 单线程 ~8MB 栈地址空间;context switch 走内核 ~1–2μs;C10K 以上破产 |
为什么必须是语言级方案:库层 stackful coroutine 无法避免栈预分配;库层 stackless(手写状态机)无法自动提升局部变量。只有编译器能在语法层面把函数切成若干 resume point,自动判断哪些变量需要跨 suspend 存活、打包到单一 frame。P0912R5(Nishanov)把这件事正式做成 C++20 语言特性。
2. 核心概念(大部分博客讲错或跳过的部分)
2.1 Stackless 是设计核心
C++20 协程是 stackless,这不是”没状态”而是一条铁律:协程只能在自己函数体的 co_await/co_yield/co_return 处挂起,不能在嵌套调用内部挂起。co_await foo() 要求 foo 要么自己也是协程、要么必须返回后再让当前协程挂起。
好处:挂起时不保存整个 C 调用栈,只保存当前函数自己的 frame。这个 frame 按函数特化、大小编译期已知。
2.2 Coroutine Frame 里装了什么
跨 suspend 存活(通常堆分配):
- promise 对象(用户定义的
promise_type实例) - 参数的拷贝(注意:引用参数不拷贝,只存引用——这是 §8 第 1 号陷阱)
- 所有跨 suspend 的局部变量和临时量
- 当前 suspend point 索引
- resume 和 destroy 函数指针(协程的”虚表”,
handle.resume()=frame->resume_fn(frame))
仅运行中存在(普通栈帧):
- 没跨 suspend 的临时量,和普通函数一样用原生栈
2.3 HALO(Heap Allocation eLision Optimization)
P0981R0 的优化:若编译器能证明协程 frame 生命周期严格嵌套在调用者内,可以把 frame 放到调用者栈上,彻底消除 operator new。
现实极其骨感:
- Clang 20+ 引入
[[clang::coro_await_elidable]]显式提示 - MSVC 相对激进
- GCC 支持最差
工程结论:热路径(百万 QPS)上不能假设 HALO 触发。三条对策:
-O2后用-fdump-tree-*/objdump -d验证是否真的消除了 new- 自定义
operator new(thread-local bump arena) - 每个会话共用少数长生命期协程,避免 per-request 新协程
2.4 三个关键字的真实含义
| 关键字 | 等价展开 |
|---|---|
co_await expr | 走 awaiter 协议,必要时挂起当前协程 |
co_yield expr | co_await promise.yield_value(expr) |
co_return expr | promise.return_value(expr) → 走 final_suspend |
co_return; | promise.return_void() → 走 final_suspend |
不能出现协程的地方:constexpr/consteval 函数、构造函数、析构函数、main、auto 推导返回类型、variadic 函数。co_await 还不能出现在 catch 块、默认参数、if/switch/for 的 init-statement 里。
2.5 promise_type 协议(真正的 API 面)
编译器通过 std::coroutine_traits<R, Args...>::promise_type 查 promise,默认是 R::promise_type。最少要实现:
struct promise_type {
auto get_return_object(); // 返回给调用者
auto initial_suspend() noexcept; // 进入函数体前是否挂起
auto final_suspend() noexcept; // co_return 后的挂起点(必须 noexcept)
void return_void(); // 或 return_value(T),二选一
void unhandled_exception(); // 抓所有从函数体抛出的异常
// 可选:
auto yield_value(T); // 支持 co_yield
auto await_transform(T); // 拦截所有 co_await(用于 sandbox)
void* operator new(size_t); // 自定义 frame 分配
static X get_return_object_on_allocation_failure(); // 不走 bad_alloc
};
2.6 Awaiter 协议
bool await_ready(); // true 跳过挂起
<ret> await_suspend(std::coroutine_handle<> h); // 挂起时做什么
T await_resume(); // 被 resume 后返回值
await_suspend 的返回类型决定挂起语义:
| 返回类型 | 语义 |
|---|---|
void | 无条件挂起,控制权返回最近的 resumer |
bool | true = 挂起并 return;false = 不挂起继续执行 |
std::coroutine_handle<> | symmetric transfer:尾调用恢复该 handle,当前栈帧先弹出再跳 |
2.7 coroutine_handle:类型擦除的非 owning 句柄
sizeof == sizeof(void*),trivially copyable。关键 API:
void resume(); // == operator()
void destroy(); // 析构 frame、释放内存
bool done() const; // final_suspend 挂起后为 true
Promise& promise() const; // 访问 promise
static coroutine_handle from_promise(Promise&);
void* address() const;
static coroutine_handle from_address(void*);
另有 std::noop_coroutine():永远 suspended 的占位 handle,symmetric transfer “终结回到 event loop” 时用。
2.8 Symmetric Transfer —— 避免链式 co_await 栈溢出的唯一方法
P0913R0(Nishanov)的关键洞见。场景:协程 A co_await 协程 B,B 同步完成后需要 resume A。朴素实现 continuation.resume() 会在原生栈上形成 B.resume(A) → A.resume(B) → ... 的递归,循环 co_await 直接栈溢出。
正确姿势:await_suspend 返回 coroutine_handle<>,编译器保证生成尾调用——当前栈帧先弹出再跳。整条 co_await 链原生栈深度保持 O(1)。
// final_awaiter 里 ——
std::coroutine_handle<> await_suspend(std::coroutine_handle<promise_type> h) noexcept {
return h.promise().continuation; // ★ 必须 return,不能 .resume()
}
3. 编译器具体做了什么
Task<int> foo(int x) { int a = co_await bar(); co_return a + x; } 被重写为大致如下(概念模型):
Task<int> foo(int x) {
// 1. 分配 frame(operator new,或 HALO 到栈)
auto* frame = operator new(sizeof(FooFrame));
frame->params.x = x; // 按值参数拷贝
new (&frame->promise) Task<int>::promise_type();
auto ret = frame->promise.get_return_object(); // 给调用者的返回值
// 2. initial suspend
co_await frame->promise.initial_suspend();
try {
// 3. body 改写成状态机
frame->a = co_await bar(); // suspend point 1
frame->promise.return_value(frame->a + frame->params.x);
} catch (...) { frame->promise.unhandled_exception(); }
// 4. final suspend
co_await frame->promise.final_suspend();
// 5. 析构 + operator delete
return ret;
}
每个 co_await 被展开为:取 awaiter(operator co_await 或 promise.await_transform)→ await_ready → 若 false,保存恢复点、调 await_suspend、return → 恢复时跳到下一个 label 调 await_resume。
4. 标准库给了什么,没给什么
| 标准提供 | 实现版本 |
|---|---|
std::coroutine_handle<P> | C++20 |
std::suspend_always / std::suspend_never | C++20 |
std::noop_coroutine() | C++20 |
std::coroutine_traits | C++20 |
std::generator<Ref, V, Alloc> | C++23(P2502R2),libstdc++ 需 GCC 14+ |
std::execution (senders/receivers, P2300) | C++26 |
标准库没给:Task<T>、executor、调度器、sync_wait、when_all / when_any、取消机制(C++20 无,P2175 推进中)、协程线程池。
工程现实:在 C++20 里裸写异步服务器,这些全部自己实现或用第三方库。
5. C++20 可编译示例(GCC 13+ / Clang 17+)
5.1 Generator:Fibonacci
// g++ -std=c++20 -O2 fib.cpp
#include <coroutine>
#include <cstdio>
template<typename T>
struct Generator {
struct promise_type {
T current;
Generator get_return_object() {
return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(T v) { current = v; return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle<promise_type> h;
~Generator() { if (h) h.destroy(); }
bool next() { h.resume(); return !h.done(); }
T value() const { return h.promise().current; }
};
Generator<long> fib() {
long a = 0, b = 1;
while (true) { co_yield a; auto t = a + b; a = b; b = t; }
}
int main() {
auto g = fib();
for (int i = 0; i < 10 && g.next(); ++i) std::printf("%ld ", g.value());
}
5.2 Task:lazy + symmetric transfer(服务器 runtime 的基石)
// g++ -std=c++20 -O2 task.cpp
#include <coroutine>
#include <exception>
#include <utility>
template<typename T>
struct Task {
struct promise_type {
T value;
std::exception_ptr exc;
std::coroutine_handle<> continuation = std::noop_coroutine();
Task get_return_object() {
return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() noexcept { return {}; } // lazy
struct final_awaiter {
bool await_ready() noexcept { return false; }
std::coroutine_handle<> await_suspend(
std::coroutine_handle<promise_type> h) noexcept {
return h.promise().continuation; // ★ symmetric transfer
}
void await_resume() noexcept {}
};
final_awaiter final_suspend() noexcept { return {}; }
void return_value(T v) { value = std::move(v); }
void unhandled_exception() { exc = std::current_exception(); }
};
std::coroutine_handle<promise_type> h;
~Task() { if (h) h.destroy(); }
// 让 Task 本身可被 co_await
bool await_ready() { return false; }
std::coroutine_handle<> await_suspend(std::coroutine_handle<> caller) {
h.promise().continuation = caller;
return h; // ★ 启动也是 symmetric
}
T await_resume() {
if (h.promise().exc) std::rethrow_exception(h.promise().exc);
return std::move(h.promise().value);
}
};
关键设计决策:
initial_suspend = suspend_always→ lazy Task:调用foo()只创建 frame,不执行代码,co_await task才启动final_suspend的final_awaiter用 symmetric transfer 跳回 continuation,整条链原生栈 O(1)await_suspend启动被 await 的 task 也用 symmetric transfer,避免栈累积~Task()里h.destroy()——这要求final_suspend必须是suspend_always,否则协程自然结束瞬间 frame 被销毁、再 destroy 就是 UB(§8 第 5 条)
5.3 IoUringAwaiter:与 reactor 集成(本项目核心)
// g++ -std=c++20 -O2 server.cpp -luring
#include <liburing.h>
#include <coroutine>
#include <functional>
struct IoUringAwaiter {
io_uring* ring;
std::function<void(io_uring_sqe*)> prep;
int result = 0;
std::coroutine_handle<> waiter;
bool await_ready() noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) noexcept {
waiter = h;
auto* sqe = io_uring_get_sqe(ring);
prep(sqe);
io_uring_sqe_set_data(sqe, this); // ★ 把 awaiter 自己塞进 user_data
io_uring_submit(ring);
}
int await_resume() noexcept { return result; }
};
// reactor 循环(单线程)
void run(io_uring* ring) {
io_uring_cqe* cqe = nullptr;
while (io_uring_wait_cqe(ring, &cqe) == 0) {
auto* a = static_cast<IoUringAwaiter*>(io_uring_cqe_get_data(cqe));
a->result = cqe->res;
io_uring_cqe_seen(ring, cqe);
a->waiter.resume(); // 回到 coroutine
}
}
// 业务代码:像写阻塞 IO 一样
Task<int> echo_once(int fd, char* buf, size_t len, io_uring* ring) {
IoUringAwaiter r{ring, [=](auto* sqe){ io_uring_prep_recv(sqe, fd, buf, len, 0); }};
int n = co_await r;
if (n <= 0) co_return n;
IoUringAwaiter w{ring, [=](auto* sqe){ io_uring_prep_send(sqe, fd, buf, (size_t)n, 0); }};
co_return co_await w;
}
业务代码是完全线性的,co_await 挂起后等 CQE 回来再 resume,写起来像阻塞调用但全程非阻塞。回调写法需要 3–5 个 lambda,协程写法 1 个函数。
6. 协程与异步 I/O 的结合(本项目重点)
6.1 为什么协程特别适合 async I/O
- 线性代码 —— 写成像阻塞一样,行为上完全非阻塞
- 无 callback 生命周期噩梦 —— 业务的所有局部变量、栈上 buffer、RAII guard 都在 coroutine frame 里,跨
co_await自动存活。对比 lambda 链必须手动shared_ptr+enable_shared_from_this - 异常正常工作 ——
co_await可以抛(由await_resume抛出),业务可以try/catch。回调模式只能透传error_code - 栈回溯部分可用 —— 单次 resume 时原生栈只有”reactor → handle.resume()“一层,但协程内部调用链(协程调协程)通过 symmetric transfer 仍能部分重建
6.2 executor / scheduler 问题(必须自己解决)
C++20 只给语言机制,没有调度器。必须决定:
- 谁跑 reactor —— 通常单线程或每 NUMA 一个
- 多线程 reactor 下协程在哪个线程 resume ——
await_suspend提交 IO 后 CQE 可能在任意 worker 线程返回,协程会在提交线程外 resume,即线程迁移(§8 第 11 条) - 如何在指定 executor 上调度 —— 通常实现
co_await scheduler.schedule()风格的 awaiter
本项目推荐:per-thread io_uring reactor + thread-local task queue。每个 worker 线程绑定一个 ring 和一组协程,协程永不跨线程迁移。SETUP_SINGLE_ISSUER | DEFER_TASKRUN(内核 6.1+)进一步锁定。
7. 库作者应掌握的模式
| 模式 | 要点 |
|---|---|
| 支持 continuation 的 Task | §5.2。必须 symmetric transfer,否则链式 co_await 栈溢出 |
sync_wait(task) | 非协程环境等协程完成。内部 receiver coroutine + binary_semaphore / condition_variable |
when_all(tasks...) | 原子递减计数器,归零时 resume 父协程 |
when_any / race | 首个完成即继续,剩余任务需要 cancellation |
| Cancellation | C++20 无原生。ad-hoc:task 持 stop_source,awaiter 在 await_suspend 注册 stop_callback 触发 IORING_OP_ASYNC_CANCEL |
| Async scope(cppcoro::async_scope) | 结构化并发,防止 fire-and-forget 协程逃逸父作用域 |
8. Pitfalls(至少 12 条,每条都见过人踩)
- 引用参数悬挂(#1 footgun)。
Task<void> h(const std::string& req)+co_spawn(h(make_req())):make_req()是临时量,co_spawnreturn 后销毁,但 frame 里只存了const string&——第一次 suspend 后引用就悬挂。规则:协程参数一律按值传。clang-tidy 有cppcoreguidelines-avoid-reference-coroutine-parameters可开。 - 临时量跨 suspend 死亡。
co_await foo(make_temp()),临时量只活到完整表达式结束,但co_await表达式可能早于协程 resume。把临时量拷进 awaiter 内部或协程函数体。 - Lambda capture 死亡。
[&x]() -> Task<int> { co_await ...; co_return x; },capture 存在 closure object 里,closure 跟 lambda 同生死,协程 frame 的this是 closure,closure 死了this悬挂。按值捕获string_view这类”引用语义”类型同样悬挂。 - eager vs lazy 选错。
initial_suspend = suspend_never立即运行到第一个 suspend;suspend_always完全 lazy。前者适合 fire-and-forget,后者适合可组合 Task。选错直接改变控制流语义。 final_suspend必须suspend_always。若返回suspend_never,frame 在协程结束瞬间 deallocate,RAII Task 析构里再h.destroy()就是 double-free / UB。这条规则 99% 情况下没有例外。- 每个协程一次
operator new。热路径(百万 QPS)上 new/delete 是瓶颈。GCC 上 HALO 基本指望不上。对策:(a) 自定义operator new(thread-local arena);(b)get_return_object_on_allocation_failure免 bad_alloc;(c) 会话级长生命期协程,避免 per-request 新协程。 unhandled_exception里 rethrow。等于在final_suspend前再次抛出,行为 UB。标准做法:current_exception()存起来,await_resume里 rethrow。- naive 链式
co_await栈溢出。不用 symmetric transfer 写 Task 的人会在final_suspend里直接continuation.resume(),同步完成 + 循环 await 立刻栈爆。务必在final_awaiter::await_suspend返回continuation而不是.resume()它。 - 调试极其困难。gdb
bt看到的栈只有 reactor → resume,看不到逻辑调用链。LLDB 17+ / GDB 15+ 有部分协程支持(frame variable可看 frame),但coro bt未标准化。生产靠人工 correlation id + trace span。 - 跨 DLL / ABI 边界。frame 布局 implementation-defined,两个 TU 用不同编译器版本就 UB。不要在库公共头文件暴露协程类型,只暴露 type-erased
coroutine_handle<>或传统回调接口。 - 线程迁移。协程在 T1 挂起(提交 SQE),T2 上 CQE 触发 resume。
thread_local、TLS 指针全变了。frame 内非 atomic 共享状态要加 memory fence(resume()本身保证 happens-before,但依赖此细节很脆)。对策:pin 协程到提交线程的 reactor,per-thread ring。 - 模板膨胀。
Task<Struct1>、Task<Struct2>… 每个实例化一份 promise/awaiter/final_awaiter 代码。大项目编译时间和二进制体积显著膨胀。减缓:Task<void>+ 共享状态,或 type-erasedUniqueTask。
9. 编译器与库支持
| 编译器 | 核心 P0912R5 | <coroutine> | std::generator (C++23) |
|---|---|---|---|
| GCC | 10 partial,11+ full(10.x 需 -fcoroutines,11+ 默认) | libstdc++ 11+ | GCC 14+ |
| Clang | 8 partial,17+ stable(-std=c++20) | libc++ 14+ | libc++ 18–19 起 |
| MSVC | 19.10 实验,19.29 (VS 2019 16.11)+ 完整(/std:c++20) | MSVC STL | VS 2022 17.13+ |
Feature test 宏:
__cpp_impl_coroutine >= 201902L(编译器)__cpp_lib_coroutine >= 201902L(库)__cpp_lib_generator >= 202207L(C++23 生成器)
10. 生态库
| 库 | 维护者 | 提供 | 适用 |
|---|---|---|---|
| cppcoro | Lewis Baker(半停滞) | task/shared_task/generator/async_generator、when_all/sync_wait、async_mutex/scope、static_thread_pool、io_service(epoll/IOCP) | 参考实现、学习 |
| libunifex | Meta | P2300 前身 sender/receiver + 协程适配 | 想用 sender 模型 |
| folly::coro | Meta | Task、AsyncGenerator、blockingWait、collectAll | Meta 栈 |
| Boost.Cobalt | Klemens Morgenstern | task、promise、generator、channel | Boost 栈 |
Asio awaitable<T> | Chris Kohlhoff | awaitable<T> + co_spawn + use_awaitable,内置 epoll/io_uring | 生产级 |
| QCoro | Daniel Vrátil | Qt signal/slot 桥接 | Qt 应用 |
| stdexec | NVIDIA | P2300 reference | 前瞻 C++26 |
本项目(raw-server)建议路线:
- 自主路线:自己写 ~500 行的
Task<T>+IoUringAwaiter+ per-thread reactor,完全掌控 symmetric transfer / 分配策略 / 调度 - 外挂路线:standalone Asio 的
awaitable<T>——成熟,但 executor 抽象稍重 - 不推荐:cppcoro 的 io_service 是 epoll 的,要自己替换成 io_uring,不如自己写
11. 横向对比:协程 vs 线程 vs 回调 vs Goroutine vs Rust async
| 维度 | Threads | C++20 coroutines | Callbacks | Stackful fibers | Go goroutines | Rust async/await |
|---|---|---|---|---|---|---|
| 每 ctx 内存 | ~8MB 虚拟 + 栈 | frame ~100B–KB | 0(无独立 ctx) | 栈 4KB–1MB | 初始 2KB segmented | Future ~KB |
| 创建成本 | 数 μs(syscall) | operator new 一次 | 分配 closure | 栈分配 | 极低(runtime 优化) | 仅构造 Future |
| 切换成本 | ~1 μs(kernel) | ~ns(函数调用 + resume) | 无切换 | 30–100 ns(汇编) | ~几十 ns | ~几 ns |
| 调试 | 完整栈回溯 | 弱(改善中) | 回调链不可见 | 特殊 gdb patch | runtime 支持 | runtime 支持 |
| 语言集成 | 原生 | 原生 C++20 | 无 | 库 | 原生 | 原生(需 runtime) |
| 生态 | 成熟 | 需第三方 runtime | 成熟 | 减少 | 标配 | tokio / async-std |
| Stackless/ful | stackful | stackless | — | stackful | stackful-ish | stackless |
协程的独特优势:stackless + 语言集成 + 零切换成本。 代价:需 runtime 胶水、HALO 不可靠、工具链不成熟。
12. 关键提案与路线图
| 提案 | 状态 | 内容 |
|---|---|---|
| P0912R5 | C++20 合入 | Coroutines TS:三关键字 + promise/awaiter 协议 |
| P0913R0 | C++20 合入 | Symmetric Transfer,await_suspend 返回 coroutine_handle |
| P0981R0 | 编译器语义基础 | HALO(但实现不一致) |
| P2502R2 | C++23 合入 | std::generator,含 ranges::elements_of 嵌套 O(1) amortized |
| P2300R10 | C++26 合入(2024-06 St. Louis plenary) | std::execution:sender/receiver/scheduler;as_awaitable 让 sender 可 co_await |
| P2175 | 推进中 | stop_token 与协程深度集成(取消) |
| P3801 | 讨论中(2025 Hagenberg) | 对 std::execution::task 的设计 concerns |
P2300 与协程的关系:互补不替代。协程是”函数怎么挂起”的语言机制;sender 是”异步计算怎么组合”的库机制。C++26 里可以 co_await sender,也可以 just(val) | then(...) 管道。
工程选型:2026 年的新项目,短期仍以协程 + 自己的 reactor 为主;C++26 铺开后(大概 2028 左右)可以迁移到 std::execution 生态。本项目的 raw-server 此时此刻应该走自主路线,见 §10。
13. 生产 Checklist
- 编译器:GCC 11+ / Clang 17+ / MSVC 19.29+;
-std=c++20 -
Task<T>::final_suspend返回suspend_always,析构里h.destroy() -
Task<T>::final_awaiter::await_suspend用 symmetric transfer(returncontinuation) - 所有协程函数参数按值传;clang-tidy 开
cppcoreguidelines-avoid-reference-coroutine-parameters - 审计 lambda 捕获:跨
co_await的 lambda 不要 by-ref 捕获栈局部 - 热路径协程评估分配开销:验证 HALO 或上自定义
operator new(thread-local arena) -
unhandled_exception用current_exception()存,不在里面 rethrow - reactor 与协程调度:per-thread io_uring + 协程 pin 到提交线程,避免线程迁移
- 协程不跨 DLL / SO 边界;公共头只暴露
coroutine_handle<>或回调接口 - 取消路径:
stop_source+stop_callback+IORING_OP_ASYNC_CANCEL组合 - 可观测性:给每个 Task 分配 correlation id,日志 / trace 按 id 聚合逻辑调用链
- 编译时间监控:
Task<T>实例化数量,必要时 type-erase
参考(authoritative sources)
- cppreference: Coroutines (C++20)
- cppreference: std::coroutine_handle
- cppreference: std::generator
- Lewis Baker 系列(协程圣经):
- P0912R5 Coroutines
- P0981R0 HALO
- P2502R2 std::generator
- P2300 std::execution
- Arthur O’Dwyer — Dangling references with coroutines
- Pablo Arias — C++20 Coroutines and io_uring
- cppcoro