C++ Coroutine in Action
基本概念
fiber
是一种轻量的线程,也常被称为“纤程”、“绿色线程”等。其作为一个调度实体接收运行时的调度。为方便使用,我们也提供了用于 fiber 的 Mutex、ConditionVariable、this_fiber::、fiber 局部存储等基础设施以供使用。使用 fiber 编程时思想与使用 pthread 编程相同,均是使用传统的普通函数(这与下文中的 coroutine 形成对比)编写同步代码,并由运行时/操作系统负责在 fiber/pthread 阻塞时进行调度。
coroutine
是一种可以被挂起、恢复(多进多出)的函数(“subroutine”)。其本身是一种被泛化了的函数。由于协程本质上依然是一个函数,因此其不涉及调度、锁、条件变量、局部存储等问题。
协程的优点
- 相比线程更加轻量
- 线程的创建和调度都是在内核态,而协程是在用户态完成的
- 线程的个数往往受限于 CPU 核数,线程过多,会造成大量的核间切换。而协程无需考虑这些
- 将异步流程同步化处理:此问题在知乎上有非常多的经典回答。尤其在 RPC 中进行多服务并发协作的时候,相比于回调式的做法,协程的好处更加明显。这个对于后端程序员的意义更大,非常解放生产力。
协程的分类
协程可以分为有栈协程和无栈协程。云风的 coroutine,微信的 libco,以及 golang 的 goroutine,都是属于有栈协程。无栈协程包括 ES6 中的 await/async、Python 中的协程等,以及 C++20 中的 Coroutine。两种协程实现原理有很大的不同。
有栈协程
一个程序要真正运行起来,需要两个因素:可执行代码段、数据。体现在 CPU 中,主要包含以下几个方面:
EIP
寄存器:用来存储 CPU 要读取指令的地址ESP
寄存器:指向当前线程栈的栈顶位置- 其他通用寄存器的内容:包括代表函数参数的
rdi
,rsi
等等。 - 线程栈中的内存内容。
这些数据内容,一般将其称为 “上下文” 或者 “现场”。
有栈协程的原理,就是从线程的上下文下手,如果把线程的上下文完全改变。即:改变 EIP 寄存的内容,指向其他指令地址;改变线程栈的内存内容等。这样的话,当前线程运行的程序也就完全改变了,是一个全新的程序。
Linux 下提供了一套函数,叫做 ucontext
簇函数,可以用来获取和设置当前线程的上下文内容。这也是 coroutine 的核心方法。
参考实现:云风 coroutine 协程库源码分析
共享栈 / 独立栈 (都属于有栈协程)
共享栈,本质就是所有的协程在运行的时候都使用同一个栈空间。
独立栈,是每个协程的栈空间都是独立的,固定大小。好处是协程切换的时候,内存不用拷贝来拷贝去。坏处则是内存空间浪费.
因为栈空间在运行时不能随时扩容,否则如果有指针操作执行了栈内存,扩容后将导致指针失效。为了防止栈内存不够,每个协程都要预先开一个足够的栈空间使用。当然很多协程在实际运行中也用不了这么大的空间,就必然造成内存的浪费和开辟大内存造成的性能损耗。
共享栈,则是提前开了一个足够大的栈空间 (云风的 coroutine 默认是 1M)。所有的栈运行的时候,都使用这个栈空间。
共享栈设计
有栈协程 (stackfull),需要保存每个协程的栈,且使用共享运行栈的方式(存在栈拷贝)。
私有协程栈
在协程专属栈空间运行,每个协程都有一个专属的私有协程栈。
- 无需栈拷贝
- 每个栈的大小固定,可能造成内存资源浪费
- 协程栈大小受限
- 对称协程,主协程负责调度,协程切出时切回主协程
共享运行栈
在协程公共栈空间运行,每个协程都有自己的栈帧信息,但是共用同一个运行栈。协程切入时,需要把其保存的栈信息拷贝到公共运行栈;切出时,再把公共运行栈的信息保存起来。
- 较大的运行栈,防止栈溢出
- 增加了栈拷贝的开销;节省内存空间
- 非对称协程,有主调和被调的关系,被调换出时切回主调,可嵌套
- 共有运行栈,需有协程专职负责栈拷贝,栈拷贝不适合在换出或换入协程处理,该协程独享一个运行栈
- 无调度协程,消息驱动
协程 Context 上下文切换
- 上下文 Context 包括:指令和栈帧
- Context 保存在专用的 CPU 寄存器中。栈基址寄存器 BP(指向栈底);栈顶寄存器 SP(指向栈顶);指令寄存器 IP(指向当前指令的下一条指令);其他寄存器
- 协程 Context 切换过程:保存主调现场,恢复另一个之前保存的现场。通过汇编实现 Context 的切换,即,保存切出协程的 Context,恢复切入协程的 Context
- 协程切换与函数调用的对比
- 相同点:指令跳转,栈帧重构,栈帧恢复
- 不同点:
- 实现方式:函数调用过程由编译器完成;协程切换需自行实现
- 调度策略:函数调用返回主调函数;协程则可切换至任一协程
- 栈帧结构:函数调用可以在系统栈实现(即,FIFO);协程调度用栈结构无法实现(不满足 FIFO)
- 生命周期:被调函数属于主调函数的一部分,调用完成则被调函数结束;协程生命周期相互独立,不管如何调度,相互可同时存在
参考方案
无栈
Boost.Asio
通过 3 个宏实现了类似协程的语义,本质上是一个用 Duff’s device 实现的 switch 语句。它是一个只有 300 多行的头文件(注释占了 200 多行)
C++20 协程
C++20 标准定义的协程,协程函数体的写法与有栈协程类似,编译器通过分析协程函数体,将协程状态和局部变量放到一块堆分配的内存上,从而转成无栈的形式。它具有极高的性能,还提供了很多 concept 可以自定义协程的行为,非常灵活
cppcoro
目前 C++20 协程只定义了框架,相关的库函数极少,cppcoro 正是这些缺失的库函数。cppcoro 定义了 task、generator、when_all 等开箱即用的高级抽象
有栈
Boost.Context
C++有栈协程库的中流砥柱,偏底层,支持ARM、MIPS、PowerPC、RISC-V、S390x、X86等平台,有着优秀的性能和稳定性。大量的有栈协程库只是对Boost.Context的封装
libco
微信开发的协程库,共享栈和 hook sys call 是其两大特色。本文使用的是 github 开源版,据说微信内部使用的版本已完全不同于开源版本
无栈+有栈
libcopp
从名字上可以看出作者的目标:libco 的 “pp版”。提供了多种协程的实现,本文涉及的是其中两个组件:
- coroutine_context:基于 Boost.Context的有栈协程,提供了更符合直觉的接口
- future:参考 rust 语言协程模型设计的无栈协程,用于平滑接入 C++20 协程,直接使用会比较繁琐。
总结
- 无栈协程的性能比有栈协程强大约 1~2 个数量级
- 但是,用到协程的场合(比如后台服务),一般来说性能瓶颈是 IO,协程的消耗可以忽略不计。这种情况下,更应该关注协程库的易用性和健壮性
- 很多有栈协程库并没有正确处理 stack unwind,在使用这些库时必须保证协程函数体优雅退出,否则可能会引起资源泄露,严重时会导致 crash
- 有栈协程库的共享栈并不是一个很好的方案,需要注意局部变量的使用,严重时会导致 crash
易用性 | 健壮性 | 性能 | |
---|---|---|---|
无栈协程 | |||
Boost.Asio无栈协程 | C | A | S |
C++20协程 | B | A | A |
cppcoro | B+ | A | A |
libcopp future | D+ | A | A+ |
有栈协程 | |||
Boost.Context | A– | B+ | B- |
libco | A+ | C | C+ |
libco共享栈 | A+ | C | C |
libcopp coroutine_context | A | B- | C |
易用性说明
- libco 由于有 co_hook_sys_call,可以将一些系统调用改成兼容协程的形式,这“可能”对某些用户很有用,额外加分
- 等级:
- A:有栈协程,可以实现业务无侵入
- B:C++20 协程,需要通过 3 个关键字 co_await、co_return、co_yield 进行控制
- C:Boost.Asio 无栈协程,需要通过 3 个宏 reenter、yield、fork 进行控制
- D:基本就是手写 switch 语句
健壮性说明
- 有栈协程因为有独立栈,天然存在栈溢出问题,所以健壮性都不如无栈协程
- 共享栈可以防止栈溢出,但共享栈存在栈对象引用问题,所以健壮性并没有额外加分
- libco 没有满足 Sys V ABI 规范的约束,额外减分,详情请看这里的分析。Boost.Context 对每个支持的平台都正确实现了对应的调用约束规范,而使用 Boost.Context 汇编代码的 libcopp 也不存在这个问题
Refer
- 为什么觉得协程是趋势?
- 微信的 libco,hook 了网络 IO 所需要大部分的系统函数,实现了当 IO 阻塞时协程的自动切换 (Golang 做的则更加极致,直接将协程和自动切换的概念集成进了语言)
- https://github.com/Tencent/libco/
- 微信 libco 协程库源码分析
- 云风实现了一套 C 语言的协程库
- https://github.com/cloudwu/coroutine/
- https://blog.codingnow.com/2012/07/c_coroutine.html
- https://github.com/chenyahui/AnnotatedCode/tree/master/coroutine (注释版本)
- https://www.cyhone.com/articles/analysis-of-cloudwu-coroutine/