基本概念

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 中,主要包含以下几个方面:

  1. EIP 寄存器:用来存储 CPU 要读取指令的地址
  2. ESP 寄存器:指向当前线程栈的栈顶位置
  3. 其他通用寄存器的内容:包括代表函数参数的 rdi, rsi 等等。
  4. 线程栈中的内存内容。

这些数据内容,一般将其称为 “上下文” 或者 “现场”。

有栈协程的原理,就是从线程的上下文下手,如果把线程的上下文完全改变。即:改变 EIP 寄存的内容,指向其他指令地址;改变线程栈的内存内容等。这样的话,当前线程运行的程序也就完全改变了,是一个全新的程序。

Linux 下提供了一套函数,叫做 ucontext 簇函数,可以用来获取和设置当前线程的上下文内容。这也是 coroutine 的核心方法。

参考实现:云风 coroutine 协程库源码分析

共享栈 / 独立栈 (都属于有栈协程)

共享栈,本质就是所有的协程在运行的时候都使用同一个栈空间

独立栈,是每个协程的栈空间都是独立的,固定大小。好处是协程切换的时候,内存不用拷贝来拷贝去。坏处则是内存空间浪费.

因为栈空间在运行时不能随时扩容,否则如果有指针操作执行了栈内存,扩容后将导致指针失效。为了防止栈内存不够,每个协程都要预先开一个足够的栈空间使用。当然很多协程在实际运行中也用不了这么大的空间,就必然造成内存的浪费和开辟大内存造成的性能损耗。

共享栈,则是提前开了一个足够大的栈空间 (云风的 coroutine 默认是 1M)。所有的栈运行的时候,都使用这个栈空间。

共享栈设计

有栈协程 (stackfull),需要保存每个协程的栈,且使用共享运行栈的方式(存在栈拷贝)。

私有协程栈

在协程专属栈空间运行,每个协程都有一个专属的私有协程栈。

  • 无需栈拷贝
  • 每个栈的大小固定,可能造成内存资源浪费
  • 协程栈大小受限
  • 对称协程,主协程负责调度,协程切出时切回主协程

共享运行栈

在协程公共栈空间运行,每个协程都有自己的栈帧信息,但是共用同一个运行栈。协程切入时,需要把其保存的栈信息拷贝到公共运行栈;切出时,再把公共运行栈的信息保存起来。

  • 较大的运行栈,防止栈溢出
  • 增加了栈拷贝的开销;节省内存空间
  • 非对称协程,有主调和被调的关系,被调换出时切回主调,可嵌套
  • 共有运行栈,需有协程专职负责栈拷贝,栈拷贝不适合在换出或换入协程处理,该协程独享一个运行栈
  • 无调度协程,消息驱动

coroutine_share_run_stack

协程 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 做的则更加极致,直接将协程和自动切换的概念集成进了语言)
  • 云风实现了一套 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/