为什么效率高?——从内存分配看
遇到C++20协程效率问题,我一般先看内存使用。协程帧在编译期固定大小,不依赖运行时栈。为什么这点重要?因为异步场景需要大量并发任务,如果每个任务都分配几MB线程栈,系统很快会耗尽内存或触发大量缺页中断。而协程帧可能只需要几百字节。这里有一段很直接的经验:“C++20协程属于无栈协程,每个协程帧的大小在编译期确定,仅包含必要的状态和局部变量。相比线程栈默认的几MB固定大小,协程帧可能只有几百字节。在需要创建大量异步任务时,这种内存节省直接减少了内存碎片和缓存未命中。检查方法是使用sizeof或编译器的协程帧分析工具观察内存占用。常见坑是将大对象在协程内按值捕获,导致协程帧膨胀,应优先使用引用或指针。”如果你发现协程切换慢,可以先查协程帧是否意外包含vector或string的完整副本。
挂起恢复开销到底有多小
第二个关注点是切换开销。协程的挂起和恢复只涉及寄存器保存,没有系统调用。用同样的话说:“协程的挂起和恢复操作被设计为只涉及必要的寄存器保存和恢复,不引入额外的系统调用或上下文切换。编译器通过promise_type和await_transform等机制将调度逻辑剥离到用户代码中。判断是否高效的标准是:如果挂起点不涉及阻塞I/O,那么协程的切换开销应低于微秒级。操作动作是避免在协程内使用标准库的std::this_thread::yield或sleep,这些会强制线程上下文切换,破坏协程的轻量特性。”一个简单的验证方法:在协程函数入口和出口记录时间戳,对比连续多次挂起的总耗时。如果每次超过几微秒,需要检查是否混入了阻塞操作或非协程等待。
容易误判的地方
最常见误判是把协程直接当成线程替代品。C++20协程本身不提供事件循环或I/O调度,需要通过libuv、Asio等框架实现。如果框架线程模型和协程恢复点不一致,可能引入数据竞争。另一个误判是认为co_await无条件高效:实际上如果嵌套过深(比如几十层),编译器可能放弃内联优化,导致代码膨胀和栈分配增多。我一般建议co_await嵌套不超过5层,超过时改用when_all或自定义组合器。还有一点:把多个不依赖的异步操作用co_await串行执行,是低效的,应该并行触发然后再等待。
如何验证协程是否真的高效
我会按顺序做以下检查:先用编译器提供的协程帧打印工具(如Clang的-fdump-coroutine-frame)观察实际帧大小。然后在挂起点前后插入轻量计数,确认挂起次数和耗时。工具上可以用perf stat统计context switches次数,如果协程大量使用但cs次数没明显增加,说明切换确实在用户态完成。日志方面,在协程恢复函数内打印线程ID,如果协程恢复时线程频繁漂移,需要检查resume是否通过executor指定了执行上下文。另一种验证是压测:用相同业务逻辑实现协程版和回调版,对比吞吐量变化。注意测试时不要混入磁盘I/O或锁竞争,否则结果不能反映协程差异。
回滚与风险
如果发现协程帧膨胀过快或出现悬垂引用,我的回退方案是先用传统回调或future接口替代,保留协程接口但内部实现改用std::async或线程池。风险边界在于悬垂引用:co_await表达式可能让协程挂起后原对象析构。操作动作是确保被等待对象的生命期大于协程,可以移动进协程帧或用shared_ptr包装。如果项目刚开始接触协程,建议先在小模块中试用,并用静态分析工具(如clang-tidy的cppcoreguidelines-avoid-capturing-lambdas)检查捕获。后续维护时定期评估协程帧大小变化,避免代码迭代引入大对象。