C++协程从入门到放弃?不,是从入门到手搓调度器
目录理解协程1. 为什么需要协程2. 协程的三个关键字3. 最简示例从编译错误开始协程的三大概念1. Promise Type承诺类型2. Awaitable 与 Awaiter 接口3. coroutine_handle协程句柄4. 手动实现一个最简单的 Task5. 手动实现一个生成器 Generator手搓协程调度器1. 为什么需要调度器2. 设计一个简易调度器3. 支持 Task 的相互等待与串链4. 调度器的线程安全结尾写 C 协程的启蒙教程最坑的就是第一个示例写一个返回 Task 的协程里面只放一句 co_return 21; 编译。——error: promise_type is not a member of Task再加一个空的 promise_type 编译。——error: get_return_object not found再补 get_return_object 编译。——error: initial_suspend ...啊合着我在填一张没尽头的破表对这就是 C 协程的见面礼标准委员会把最小核心留给了我们然后强制要求我们必须实现这堆玩意不然就是报错报错还是报错。理解协程1. 为什么需要协程早年间服务器写逻辑多爽啊一个线程处理一个连接代码从上到下 read(fd, buf, size) 一阻塞操作系统帮我们切出去醒过来数据就在buf里了这看起来很完美。那么问题在哪儿线程是稀缺的物理资源每个线程都得占内核栈、上下文切换要刷 TLB一万个连接就是一万个线程我们那64GB内存的服务器瞬间变烤箱还得时刻提防一些小问题。于是大家开始搞异步epoll 配上非阻塞 IO一个线程管上万个连接代码就变成了void on_readable(int fd) { // 读取一部分数据... if (数据没收完) { // 注册继续读的回调返回事件循环 return; } // 处理数据... // 发送响应注册写回调... }逻辑被撕成一地碎片每次我们只能执行一小段然后出让控制权等下次事件触发再接着跑下一段。这种回调方法状态要么靠全局变量逃逸要么塞进一个 connection 结构体里传来传去。读写一个完整的 HTTP 请求我们那原本20行的同步代码现在变成四个回调函数彼此间靠一个 state 枚举相认stateHEADERstateBODY...改 bug 的时候我们的表情和那枚举值一样扭曲。然后协程来了它本质上就是让我们用同步的写法享受异步的性能。协程是可暂停的函数我们写std::string line co_await async_read_until(socket, \n); co_await async_write(socket, ...);编译器会把这两个 co_await 之间的代码切成三段自动生成一个状态机把局部变量存到堆上所以协程帧通常在堆上分配编译器在满足条件时可优化为栈上分配HALO。暂停的时候控制权返回给调用者/事件循环事件就绪后再从这个暂停点恢复执行。代码形态是同步的但执行时是异步的线程不用傻等我们也不用跟几十个回调函数玩耍。协程让我们既保住了线程的高效复用又找回了顺序编写业务逻辑的体面。它把我们那堆破事状态保存、控制流转移料理得明明白白还不用我们给每个事件写一堆代码。好了现在是不是觉得协程简直是救世主别急这里是 C它的实现方式马上让我们哭。2. 协程的三个关键字C20 给协程加了三个关键字co_await、co_yield、co_return。它们不是功能只是标记。函数体里只要出现任何一个这个函数就被编译器判定为协程然后编译器会掀翻我们的代码强插一大堆东西。co_await暂停点等待一个可等待体就绪。如果没就绪协程让出执行保存当前状态回头再从这里恢复。co_yield暂停并产出一个值用来做生成器比如懒序列生成。我们每次 for (auto x : gen()) 循环迭代协程就跑一步co_yield 一个值给我们然后立刻暂停等下一次循环再唤醒。co_return协程结束返回最终值。这和普通 return 不同它不会做栈展开销毁局部变量那是协程帧销毁时做的。普通 return 在协程函数里属于违规操作编译器会直接甩脸子。就三个关键字没了。我们以为学三个单词就够了太天真了C 表示“给你最小核心剩下你自己用库实现”。所以这三个关键字只是冰山露出水面的尖水底下是 promise_type、coroutine_handle、awaiter 这一整套定制点。别的语言里的协程开箱即用C 嘛自由是自由但该手搓的地方就得手搓。3. 最简示例从编译错误开始来我们写个最简单的协程想返回一个 Task 对象能 co_return 一个整数#include coroutine struct Task { // 啥都没写先占个坑 }; Task my_coroutine() { co_return 21; }我们兴冲冲按了编译然后编译器立刻把我们摁在地上摩擦error: no type named promise_type in Task编译器内心 OS“你说 Task 是协程返回类型那好按照规矩 Task 内部必须得有个叫 promise_type 的类型你没给我这协程我生不出来。”为什么因为 C 协程是无栈协程它的状态局部变量、暂停点和我们看到的返回类型是解耦的。当一个函数被判定为协程编译器自动生成类似这样的伪代码Task my_coroutine() { using promise_t Task::promise_type; // 必须从返回类型里萃取 auto* frame new coroutine_frame(promise_t{}); // 在堆上分配协程帧 promise_t promise frame-promise; // ... 把我们的函数体拆成状态机塞进去 ... // 返回 Task 对象由 promise.get_return_object() 创建 return promise.get_return_object(); }所以 Task 必须提供一个 promise_type这个 promise_type 里还需要实现一堆接口get_return_object()、initial_suspend()、return_value()…… 少一个都过不了。我们忍着气加上最小实现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_value(int val) { /* 存起来 */ } void unhandled_exception() { std::terminate(); } }; };现在再编译通过了但我们运行什么都没干因为 get_return_object() 返回了个空壳值丢了。而且我们发现 Task 对象还必须是可移动或可复制的否则协程帧返回它时也会报错。更骚的是 final_suspend 如果返回 suspend_always协程结束后控制权还给调用者但协程帧还没释放需要我们手动 destroy()否则内存泄漏这就需要 coroutine_handle 这个烫手山芋。一个 hello world 级别的协程强制要求我们理解 promise_type、awaiter、协程句柄生命周期。这就是 C 的仪式感我们在写出第一行有效协程代码之前必须亲手为它搭建框架然后编译器笑眯眯地站旁边看我们调试段错误。抱怨归抱怨这条陡峭的学习曲线恰恰是 C 协程能接近零开销抽象的代价它把调度、分配、异常处理全部开放给库作者所以我们能定制出高性能的 generator、async_task甚至把协程帧分配在自己发明的内存池里。这比虚拟机里靠全局锁的协程无意冒犯某些语言自由得多代价就是第一眼看到它的时候我们以为写了个bug其实是特性。协程的三大概念1. Promise Type承诺类型C 协程的 promise_type 是协程的内部控制中心。每次我们声明一个返回类型为 Task 的协程函数编译器就会跑到 Task 结构里找 Task::promise_type然后把它实例化出来塞进协程帧。它强制我们实现的接口每一个都对应协程生命周期的一个节点struct promise_type { // 协程一启动先调这个返回一个 awaiter 决定是否立即挂起 auto initial_suspend(); // 协程结束co_return 或异常调这个返回 awaiter 决定是否最终挂起 auto final_suspend() noexcept; // 怎么把 promise 和返回的 Task 对象关联起来靠这个 Task get_return_object(); // 处理 co_return 值或 void void return_value(T val); void return_void(); // 如果协程里抛异常且没被捕获会走到这里 void unhandled_exception(); // 如果用 co_yield还得有这个 auto yield_value(T val); };有多少人第一次看到 initial_suspend 返回 std::suspend_never 心里想的是“能不能直接删掉”。答案是不能因为标准委员会非要给我们这个切入时挂起的灵活性仿佛每个协程都希望在起步阶段先思考一下人生。promise_type 和协程之间是通过承诺通信的协程函数体里的 co_return 21 最终变成 promise.return_value(21)如果抛出异常且我们没 catch就变成 promise.unhandled_exception()然后一般我们在里面 std::current_exception() 拿到异常并妥善保管以免协程静默挂掉留一堆内存泄漏。2. Awaitable 与 Awaiter 接口这是协程能等东西的核心协议co_await 后面可以跟三种东西直接是一个实现了 await_ready()、await_suspend()、await_resume() 的类就是 Awaiter。一个重载了 operator co_await() 的类型返回一个 Awaiter。通过 promise 的 await_transform() 转换得到 Awaiter。大部分高阶封装都是在 await_transform 里做文章让我们能 co_await 另一个协程。但咱们先把最原始的 Awaiter 拆开struct Awaiter { bool await_ready(); // 如果返回true根本不挂起直接往下走 void await_suspend(coroutine_handle); // 决定挂起后马上调这个可以在这里把 handle 注册到 epoll/事件循环 T await_resume(); // 恢复时调返回值直接给co_await表达式 };await_suspend 可以返回 void、bool 或另一个 coroutine_handle。返回 true 代表立即恢复不用真挂起返回 false 代表挂起等唤醒返回另一个协程句柄那就是对称转移从当前协程直接切过去控制权不回到调用方这是高性能无栈协程调度的精髓也是把调试器看到脑裂的根源。编译器在 co_await 处生成的代码大致是“先调 await_ready如果false就保存当前状态调 await_suspend(h)把控制权返还给调用者或调度器将来有人调 h.resume()再调 await_resume 把值交出来。”3. coroutine_handle协程句柄我们可以简单理解为指向协程帧的裸指针但带类型安全。它是一个类似 void* 但知道怎么 resume/destroy 的东西。coroutine_handlepromise_type特定协程的句柄能拿到 promise 引用。coroutine_handle无类型可以 resume但拿不到 promise适合当通用回调参数。它身上我们必用的几个操作h.resume()恢复从暂停点继续执行。h.destroy()释放协程帧堆上的那坨状态机。h.promise()获得 promise_type 引用从而访问里面的结果或异常。h.done()检查协程是否已经结束final_suspend 之后就是 done 状态。如果 final_suspend 返回 suspend_always那么协程结束时会挂起在最终点帧不自动释放我们必须手动调 destroy()否则内存泄漏。有时候我为了省事用 suspend_never那样协程结束自动销毁但带来一个问题我们如果持有 coroutine_handle 还想事后访问结果帧已经被释放了悬垂指针伺候。这就是为什么正经异步 Task 都用 suspend_always 做 final suspend让调用者在取完结果后显式 destroy()。另外 coroutine_handle 本身是个类似指针的小对象拷贝它只是多了个引用不做引用计数。我们在异步回调里持有它必须保证协程帧在回调执行时还活着否则我们就是在用 C 堵你的枪里没有子弹。4. 手动实现一个最简单的 Task这里我们就做一个能 co_return 的 Task结果存起来外部能拿没有调度器同步方式使用templatetypename T struct Task { struct promise_type { T value; // 存结果 std::exception_ptr except; // 存异常 Task get_return_object() { // 从promise构造Taskhandle后续跟promise关联 return Task{ std::coroutine_handlepromise_type::from_promise(*this) }; } std::suspend_never initial_suspend() { return {}; } // 不挂起直接开跑 std::suspend_always final_suspend() noexcept { return {}; } // 最后挂起等之后手动销毁 void return_value(T v) { value v; } void unhandled_exception() { except std::current_exception(); } }; std::coroutine_handlepromise_type handle; Task(std::coroutine_handlepromise_type h) : handle(h) {} Task(const Task) delete; Task operator(const Task) delete; Task(Task other) noexcept : handle(other.handle) { other.handle nullptr; } Task operator(Task other) noexcept { if (this ! other) { if (handle) handle.destroy(); handle other.handle; other.handle nullptr; } return *this; } ~Task() { // 因为final_suspend是suspend_always所以done时帧还在需要销毁 if (handle) handle.destroy(); } T get_result() { if (handle.promise().except) std::rethrow_exception(handle.promise().except); return handle.promise().value; } }; // 使用 Taskint compute() { co_return 21; } int main() { auto t compute(); // 协程在initial_suspend不挂起直接执行完然后在final_suspend挂起 std::cout t.get_result() std::endl; // 21 // t析构时destroy }我们只是为了 co_return 21 就写了一坨东西出来是不是觉得非常反人类但这就是 C给予了我们自由——能决定 final_suspend 是挂起还是自动销毁也能决定 get_return_object 是直接构造还是返回个惰性 Future。5. 手动实现一个生成器 Generator生成器是另一个流派它每次 co_yield 一个值然后暂停让调用者通过迭代器拿值调用者 时恢复协程继续往下跑直到 co_return 结束。我们得让 Generator 支持范围 for 循环就得实现一个迭代器里面持有一个 coroutine_handle用 resume 驱动。templatetypename T struct Generator { struct promise_type { T current_value; std::exception_ptr except; Generator get_return_object() { return Generator{ std::coroutine_handlepromise_type::from_promise(*this) }; } std::suspend_always initial_suspend() { return {}; } // 开始先挂起等迭代器resume std::suspend_always final_suspend() noexcept { return {}; } std::suspend_always yield_value(T v) { current_value v; return {}; // 挂起 } void return_void() {} void unhandled_exception() { except std::current_exception(); } }; struct iterator { std::coroutine_handlepromise_type h; bool done; T operator*() const { if (h.promise().except) std::rethrow_exception(h.promise().except); return h.promise().current_value; } iterator operator() { h.resume(); // 让协程跑下一个yield点 done h.done(); return *this; } bool operator!(const iterator other) const { return done ! other.done; } }; std::coroutine_handlepromise_type handle; Generator(std::coroutine_handlepromise_type h) : handle(h) {} Generator(const Generator) delete; Generator operator(const Generator) delete; Generator(Generator other) noexcept : handle(other.handle) { other.handle nullptr; } ~Generator() { if (handle) handle.destroy(); } iterator begin() { if (!handle || handle.done()) return { nullptr, true }; handle.resume(); // 启动协程跑到第一个yield点 return { handle, handle.done() }; } iterator end() { return { nullptr, true }; } }; // 示例 Generatorint fib(int n) { int a 0, b 1; while (n--) { co_yield a; int tmp a; a b; b tmp; } co_return; } // 使用 for (auto x : fib(10)) std::cout x ;这里 initial_suspend 返回 suspend_always是为了让协程在创建后先挂起等 begin() 里手动 resume 才真正跑起来这就是惰性生成器。如果用 suspend_never协程会在构造时就跑到第一个 co_yield如果没提前做好调用环境比如在构造后就析构走了容易逻辑混乱所以生成器几乎清一色 initial_suspend suspend_always。还有yield_value 返回 suspend_always 表示每次产出一个值后必须挂起等待外部取用。如果我们返回 suspend_never那协程就会无视调用者一股脑跑到完那就不叫生成器叫脱缰的野马。手搓协程调度器我们把协程的骨头架子搭起来了Task 能跑了Generator 也能产出了但它的灵魂调度能力还是一片空白。我们现在写的 TaskT 一启动就一头撞进 initial_suspend 直接跑完或挂起等我们手动 resume而且完全不知道怎么跟 IO 事件卿卿我我。1. 为什么需要调度器你可能会问“我直接在 await_suspend 里把 coroutine_handle 丢给 epoll事件来了 resume 一下不就完事了”哦~香蕉啊这太天真了这种直连方式的确能跑一个协程但稍微一复杂就爆炸多协程协调协程 A 想等协程 B 的结果谁去 resume A如果是 B 直接在 final_suspend 里 resume A那 B 的协程帧还没销毁呢控制权就跳到 A 了——对称转移爽是真爽但一旦出错我们根本不知道该怪 A 还是怪 B堆栈跟踪比我们的感情线还乱。优先级和公平性一百万个协程同时就绪我们全在一个 for 循环里 resumeCPU 瞬间爆炸而且有的协程可能饿死。线程安全多个线程同时完成 IO同时去 resume 同一个协程恭喜喜提数据竞争精准踩中协程本身不是线程安全的红线。资源管理调度器才是知道“哪些协程正在飞哪些在等”的唯一权威销毁时机、取消、超时全得靠它居中调停。所以调度器的本质是一个协程句柄的执行上下文。它持有一组就绪的协程句柄决定下一个该执行谁它提供一个挂载点让 await_suspend 把当前协程挂起并注册唤醒回调它负责在恰当的时机如 IO 就绪、锁释放把协程句柄重新塞回就绪队列。没有调度器的协程就像没有 shell 的 Linux 内核——能编译但没法交互。2. 设计一个简易调度器我们就从最土的版本开始单线程、纯用户态、无 IO。调度器只干一件事维护一个就绪队列里面是待执行的 coroutine_handle然后一个主循环不断取出执行直到队列空。先定义调度器类因为是单线程连锁都不要class SimpleScheduler { public: void schedule(std::coroutine_handle h) { ready_queue.push(h); } void run() { while (!ready_queue.empty()) { auto h ready_queue.front(); ready_queue.pop(); h.resume(); // 驱动协程跑一小段直到下个挂起点 } } private: std::queuestd::coroutine_handle ready_queue; };就这么几行已经是一个能用的协程调度器了。它用 std::queue 保证先就绪的协程先跑简单公平不会饿死。那怎么让我们的 TaskT 跟它配合关键就在于 await_suspend。之前我们写的 struct Awaiter 需要拿到这个调度器引用并在目标协程完成时把挂起的协程句柄安排回去。但咱先不急为了感受一下调度的威力我们可以写一个最小的可调度 Awaiter以及一个绑定调度器的 Task。这次我们不追求什么封装就先粗鄙地用一个全局调度器指针请原谅我的粗糙SimpleScheduler* g_scheduler nullptr; // 全局调度器 struct SchedulerAwaiter { std::coroutine_handle awaited; bool await_ready() { return false; } // 总是挂起 void await_suspend(std::coroutine_handle h) { awaited h; g_scheduler-schedule(h); } void await_resume() {} };这个 SchedulerAwaiter 实际上就是个“让我暂时让出 CPU回头再继续”的耍赖写法。更实际的用法是在 await_suspend 里把 h 存起来不马上调度而是等某个异步事件完成后再 schedule(h)比如 IO 就绪回调里做这件事。现在展示一个显式与调度器配合的示例创建一个协程内部 co_await 一个自定义的 Awaiter这个 Awaiter 需要调度器稍后再唤醒它。可以实现一个 async_sleep 函数返回一个 Awaiter它在 await_suspend 里把一个定时任务扔给调度器。这里我们就不搞这么复杂的就先模拟一下直接 schedule(h) 让协程立刻就绪跑个空转体验一下调度器运转。这虽然毫无意义但能让我们看到流程。// 一个总是立即再次就绪的 awaiter struct YieldAwaiter { bool await_ready() noexcept { return false; } void await_suspend(std::coroutine_handle h) noexcept { // 不立即resume而是塞回调度器队尾 g_scheduler-schedule(h); } void await_resume() noexcept {} };然后在协程里Taskint some_coro() { std::cout 协程启动\n; co_await YieldAwaiter{}; // 让出执行权被调度器重新入队 std::cout 从 yield 中回来\n; co_return 21; } int main() { SimpleScheduler sched; g_scheduler sched; auto t some_coro(); std::cout run()\n; sched.run(); }输出协程启动run()从 yield 中回来跑起来后我们会看到这些输出证明协程被暂停又被调度器拉起来再跑完了。虽然只是在自己玩但我们亲手摸到了“在挂起点把控制权归还给调度器再由调度器恢复”的完整链路。以后我们要上 IO只需把 schedule(h) 的调用挪到 epoll 事件回调里即可。3. 支持 Task 的相互等待与串链这才是调度器的真正价值协程 A 等待协程 B 的结果B 完成时A 自动被调度器唤醒继续执行。因为单线程下不需要锁我们有两种选择对称转移B 完成时直接 resume A 的句柄控制权不经过调度器效率高但调用栈可能炸。调度器版本B 完成时把 A 的句柄 schedule 到调度器的就绪队列然后 B 继续执行完销毁调度器稍后挑出 A 来跑。这种会多一次入队出队但线程安全扩展时更容易控制。我们使用调度器版本感受一下调度器编排多个协程的魔力。依旧用全局 g_scheduler 来简化先来修改 TaskT 的 promise_type加入 continuation 字段struct promise_type { T value; std::exception_ptr except; std::coroutine_handle continuation; // 等待本协程完成的那个协程句柄 // ... 其他不变 ... std::suspend_always final_suspend() noexcept { // 本协程结束如果有等待者则把它调度好 if (continuation) { g_scheduler-schedule(continuation); // 唤醒等待者 } return {}; // 仍然suspend_always等外部destroy } };然后实现 TaskT 的 Awaiter如果 other 已经完成即协程 done()那就不该挂起直接取结果走人templatetypename T struct Task { // 其它定义略 // 内部 awaiter struct Awaiter { std::coroutine_handlepromise_type src_h; // 被等的协程句柄 bool await_ready() { // 如果源协程已经完成那就不用挂起直接可以取结果 return src_h.done(); } auto await_suspend(std::coroutine_handle caller_h) { // 把caller_h注册到源协程的promise里等待唤醒 src_h.promise().continuation caller_h; } T await_resume() { // 恢复时源协程的结果一定已经就绪从promise里取 auto promise src_h.promise(); if (promise.except) std::rethrow_exception(promise.except); return promise.value; } }; // 给 co_await task 用 auto operator co_await() { return Awaiter{ handle }; } };await_suspend 里我们把 caller_h 存到 src_h.promise().continuation 后不需要手动调度任何东西只是返回 void表示挂起。控制权此时回到哪里看谁调用了 h.resume() 驱动了协程。如果是调度器驱动那 await_suspend 返回后调度器会继续运行其他就绪协程。稍后当源协程src_h完成它的 final_suspend 被调用看到 continuation 非空就把 caller_h schedule 进就绪队列于是等待者被唤醒。整理一下完整的调度器驱动流程调度器 run() 循环 dequeue 一个 handle。协程执行到 co_await other_task进入 Awaiter::await_suspend把当前句柄存进 other_task 的 promise然后返回 void挂起。resume() 返回到调度器的 while 循环。调度器继续取下一个就绪协程运行。当 other_task 执行到最后 final_suspend发现 continuation 非空schedule(continuation)把这个等待句柄放回就绪队列。调度器后来又取出这个句柄从 await_resume 拿到结果后继续执行。使用示例// 一个辅助 awaiter让出当前执行权 struct ScheduleAwaiter { bool await_ready() { return false; } // 总是挂起 void await_suspend(std::coroutine_handle h) { g_scheduler-schedule(h); } void await_resume() {} }; auto schedule() { return ScheduleAwaiter{}; } // 示例协程 Taskint compute_slow(int base) { std::puts(compute_slow 开始计算); co_await schedule(); // 主动让出稍后恢复 std::puts(compute_slow 恢复执行); co_return base * 2; } Taskint use_result() { std::puts(use_result 准备调用 compute_slow(5)); int val co_await compute_slow(5); // 挂起等待 compute_slow 完成 std::puts(use_result 恢复得到结果并处理); co_return val 3; } int main() { SimpleScheduler scheduler; g_scheduler scheduler; auto task use_result(); scheduler.run(); // 调度所有协程直到全部完成 int result task.get_result(); // 取回最终返回值 std::cout 最终结果 result \n; }输出use_result 准备调用 compute_slow(5)compute_slow 开始计算compute_slow 恢复执行use_result 恢复得到结果并处理最终结果13这就是链式 Task 等待的一个模型。4. 调度器的线程安全单线程调度器已经能支撑大部分 IO 密集型应用但当我们需要利用多核或者有阻塞操作要放进线程池时就必须让调度器线程安全。首先明确一个雷区同一个协程绝不能被多线程同时 resume。协程本身不是线程安全的它的执行应该被绑定在某一个线程上。因此线程安全扩展通常不是让一个调度器多线程运行而是采用调度器组模式多个线程中每个线程拥有自己私有的调度器协程被创建时指定跑在哪个线程的调度器上如果需要跨线程通信就通过线程安全的队列把 coroutine_handle 投递到目标线程的调度器里唤醒。最简单的扩展给我们的 SimpleScheduler 加上一个线程安全的投递队列其他线程想唤醒一个协程就把句柄投递到这个队列而调度器主循环除了处理自己私有的 ready_queue还要定期排空这个全局共享队列。class SafeScheduler { public: void schedule(std::coroutine_handle h) { std::lock_guard lk(mut); queue.push_back(h); cv.notify_one(); } void run() { while (true) { std::coroutine_handle h; { std::unique_lock lk(mut); cv.wait(lk, [this]{ return !queue.empty() || stop; }); if (stop queue.empty()) return; h queue.front(); queue.pop_front(); } h.resume(); } } void request_stop() { std::lock_guard lk(mut); stop true; cv.notify_all(); } private: std::dequestd::coroutine_handle queue; std::mutex mut; std::condition_variable cv; bool stop false; };这已经是一个多生产者-单消费者的协程调度器多个工作线程和 IO 线程都可以把就绪协程投递进来主调度线程安全地去 resume。听起来很美但藏着几个坑一旦队列上有竞争吞吐量会下降高性能方案通常用无锁队列。如果一个协程原本在调度器 A 的线程上跑后来被调度器 B 的线程投递和 resume那它在不同线程上执行了里面的 thread_local 数据全乱套。所以设计时就应明确协程绑定线程的语义或禁止迁移。跨线程调度意味着有可能在 schedule(h) 之后、目标线程还没 resume 之前协程帧被其他线程销毁了然后目标线程 resume 就炸。这就要求协程帧的所有权必须明确或使用引用计数。所以上了锁的调度器不是万能的不到万不得已不要跨线程 resume。结尾C20 的协程只是一个开始还有许多的不足很多东西都要我们自己实现。不过 C26 已经把 std::execution 塞进标准用于管理通用执行资源上的异步执行。但在能使用 C26 写项目之前想用协程写生产代码我们只有三条路找一个经过充分测试的第三方库比如 folly::coro或者自己手搓一个并承担所有边界条件或者干脆等着——不过说实话等标准委员会给我们把这些配齐可能自家孩子都学会写协程了。