认识C++无栈协程

2023/04/20 协程 共 2158 字,约 7 分钟

写在前面

虽然C++20才开始慢慢支持协程,但是协程并不是一个新的概念,出现了好几十年,其他诸多语言比如goC#很早就支持协程了。

协程分为无栈协程和有栈协程两种,无栈指可挂起/恢复的函数,有栈协程则相当于用户态线程。有栈协程切换的成本是用户态线程切换的成本,而无栈协程切换的成本则相当于函数调用的成本,C++20协程选择的是无栈协程。

协程和线程的区别:协程只能被线程调用,本身并不抢占内核调度,而线程则可抢占内核调度。协程的切换是用户决定的,不进入内核态。

C++20的无栈协程相当于是泛化的函数,通过把函数的Call以及Return(其中包括Exception)细分出suspend、Resume、Destory。简单理解C++20的无栈协程相当于是可以挂起和恢复的函数。

why stackless coroutine

有栈(stackful)协程通常的实现手段是在堆上提前分配一块较大的内存空间(比如 64K),也就是协程所谓的“栈”,参数、return address 等都可以存放在这个“栈”空间上。如果需要协程切换,那么通过 swapcontext 一类的形式来让系统认为这个堆上空间就是普通的栈,这就实现了上下文的切换。

有栈协程最大的优势就是侵入性小,使用起来非常简便,已有的业务代码几乎不需要做什么修改,但是C++20最终还是选择了使用无栈协程,主要出于下面这几个方面的考虑。

栈空间的限制

  • 有栈协程的“栈”空间普遍是比较小的,在使用中有栈溢出的风险;而如果让“栈”空间变得很大,对内存空间又是很大的浪费。无栈协程则没有这些限制,既没有溢出的风险,也无需担心内存利用率的问题。

性能

  • 有栈协程在切换时确实比系统线程要轻量,但是和无栈协程相比仍然是偏重的,这一点虽然在我们目前的实际使用中影响没有那么大(异步系统的使用通常伴随了 IO,相比于切换开销多了几个数量级),但也决定了无栈协程可以用在一些更有意思的场景上。举个例子,C++20 coroutines 提案的作者 Gor Nishanov 在 CppCon 2018 上演示了无栈协程能做到纳秒级的切换,并基于这个特点实现了减少 Cache Miss 的特性。

无栈协程是一个可以暂停和恢复的函数,是函数调用的泛化。为什么?我们知道一个函数的函数体(function body)是顺序执行的,执行完之后将结果返回给调用者,我们没办法挂起它并稍后恢复它,只能等待它结束。而无栈协程则允许我们把函数挂起,然后在任意需要的时刻去恢复并执行函数体,相比普通函数,协程的函数体可以挂起并在任意时刻恢复执行。所以,从这个角度来说,无栈协程是普通函数的泛化。

img

stackless coroutine

C++20的无栈协程可以在不破坏激活帧的情况下暂停,因此我们不能再保证激活帧的生存期将被严格嵌套。意味着不能和调用函数一样把帧放在栈中,因此协程帧分配在堆上的。

但C++协程TS中有一些规定,如果编译器能够证明协程的生存期确实严格嵌套在调用程序的生存期内,则允许从调用程序的激活帧分配协程帧的内存。作用域不跨越任何协程挂起点的变量的生存期可能存储在栈上。

前面说到,协程就相当于一个泛化的操作,因为除了Call和Return以外,还有suspend、Resume、Destory等操作。

The ‘Suspend’ operation

  • 协同程序的挂起操作允许协同程序在函数中间挂起执行,并将执行传递回协同程序的调用方或恢复方。一般使用co_await 或者co_yield来完成这个操作 。

The ‘Resume’ operation

  • 和普通函数调用一样,resume调用分配一个栈,然后读取所需要的参数,不过,它不会从函数刚开始执行,而是从上次函数挂起(suspend)的点开始执行。

The ‘Destroy’ operation

  • Destroy操作的作用与Resume操作非常相似,因为它重新激活协程的激活帧,包括分配一个新的堆栈帧和存储Destroy操作调用方的返回地址。但是,它不是在最后一个挂起点将执行转移到协程主体,而是将执行转移至另一个代码路径,该代码路径在挂起点调用作用域中所有局部变量的析构函数,然后释放协程框架使用的内存。

The ‘Call’ operation of a coroutine

  • 协程的调用和普通操作的调用在调用者看来几乎没什么区别。但是,使用协程将会在协程到达挂起点的时候恢复调用方的执行。当对协程执行Call操作时,调用者会分配一个新的堆栈帧,将参数写入堆栈帧,向堆栈帧写入返回地址,并将执行转移到协程。这与调用正常函数完全相同。但是,协程还会做另一件事,在堆上分配一个协程帧,并将参数从堆栈帧复制/移动到协程帧中,以便延长参数的生存期。

The ‘Return’ operation of a coroutine

  • 当协程执行返回语句的时候,(往往使用co_return),用户可以自定义一些行为存储返回值,方便之后取出。然后销毁除了参数以外的所有局部变量。然后会执行一些自定义操作,最终执行被转移回调用方/恢复方。

reference

Coroutine Theory

C++20 协程原理和应用

C++20 新特性 协程 Coroutines(1) - 知乎 (zhihu.com)

C++20 新特性 协程 Coroutines(2) - 知乎 (zhihu.com)

Search

    Table of Contents