当年我们第一次学异步。
总会有一种幻觉。
“只要我写成异步,就会更快。”
后来你会发现。
快不快是一回事。
能不能排障。
是另一回事。
协程把代码写直了。
但它不是免费的。
当年:线程也很美,但线程也会咬人
你开线程。
看起来也很直。
函数从上到下。
只是跑在另一个线程。
然后你就遇到锁。
遇到竞态。
遇到死锁。
协程的出现。
某种程度上也是一种工程妥协。
“我想要像同步一样的代码。”
“但我不想付出线程那套代价。”
线上啪一下:你以为没开线程,结果栈跑丢了
你写了协程。
线上出问题。
你打了个断点。
或者看崩溃栈。
你发现栈不像以前那样一条直线。
它中间会断。
因为协程可以挂起。
挂起后。
后续逻辑不在同一个调用栈里。
这不是 bug。
这是它的工作方式。
代价 1:你可能会有堆分配
很多协程实现。
会把协程帧放到堆上。
因为它要活过挂起点。
你可以把协程帧理解成函数暂停时需要保存的现场。
参数、局部变量和下一条要执行的语句位置都在里面。
协程不是“零成本抽象”。
代价 2:生命周期和取消,比你想的麻烦
回调时代。
你很清楚。
没回调就没后续。
协程时代。
你可能会写出这种逻辑。
Task<void> f() {
auto r = co_await HttpAwaitable{"/config"};
(void)r;
co_return;
}
看起来很干净。
但你要问自己。
如果调用方不再关心结果。
这个协程怎么办。
它要不要取消。
要不要把底层请求也取消。
这些都不是语法能替你决定的。
代价 3:调试体验要看工具链
有些环境。
协程的调试很好。
有些环境。
你只能看到一堆内部框架函数。
你以为你在 debug 业务。
结果你在 debug runtime。
这很折磨。
代价 4:ABI 与库生态
C++20 标准化了语法。
但没有标准化一个“官方的 task / generator”。
所以你很容易看到。
A 库有自己的 Task。
B 库有自己的 Task。
它们看起来都叫 Task。
但互相不兼容。
这就是生态现实。
什么时候用协程
协程很适合。
你有很多“等待”。
而你又想把控制流写成直线。
比如网络。
比如 IO。
比如事件驱动。
什么时候别硬上
如果你的系统本来就很简单。
没有太多异步。
那你引入协程。
可能只是在引入复杂度。
协程是工具,不是信仰。
小结
协程最值钱的地方。
是把控制流从树变回直线。
它最贵的地方。
是你得为“挂起”买单。
懂这两句。
你就不会被协程反噬。