什么是coroutine?接触过的脑子里确定会蹦出来不少词:async-await,generator,channel,yield,高并发,甚至goroutine。其实,这些都是coroutine的外部表象,coroutine的本质是什么?上古时期的计算机科学家们早就给出了概念,coroutine就是能够中断并恢复执行的subroutine,什么是subroutine?就是你们熟知的函数。前端
你们先不要在脑子里思考这个中断和恢复执行具体要怎么作,而是先创建一个概念模型,一个函数除了调用结束后返回caller,还能够在调用中途返回caller,并能够由caller在中断的地方继续执行,这就是coroutine了。因此coroutine也被叫作resumable function,可继续函数,而且从定义上看,coroutine是subroutine的超集,也就是全部的function也都是coroutine,只不过他们没有执行中断和继续这两个操做。并发
因此看到这里,先不要想太多,先明确了coroutine是能够中断并恢复的函数就行了,咱们而后来明确几个概念,为了后面提到的时候不会混乱。less
coroutine的暂停/中断,也叫suspend,是coroutine暂停执行,并将控制流返回给调用者caller的过程。async
coroutine的恢复/继续,也叫resume,是caller恢复coroutine执行的过程。函数
固然coroutine还包含了普通函数也有的调用(invoke)和返回(return)两个操做。高并发
函数要中断并从中断处恢复,那么从常识上考虑,就像线程切换cpu要保存寄存器状态同样,函数中断前也要保存当前的状态到一个持久的位置,而后中断后这部分栈空间才能放心的交给caller去继续用,否则恢复的时候现场都被破坏了就不对了。性能
保存当前状态有两种常见的实现,一种是说,既然函数中断以后栈空间不能被别人改写,寄存器的值要保存下来,那不如让这个函数使用独立的栈空间好了,这种实现就是有栈协程(stackful-coroutine)。函数调用前,保存调用者的全部寄存器,而后malloc一块单独的空间并把栈指针指过去,而后正常调用函数,被调用的函数天然就会在这块独立的栈空间上操做。中断前,把全部寄存器都存到栈上,而后把栈指针指回调用者的栈空间,而且恢复调用者的寄存器,这就实现了有栈协程的暂停操做。优化
根据个人描述也能看出,有栈协程的实现须要底层操做,好比修改栈指针,保存寄存器等等,并且有栈协程须要大块的栈空间分配,无论什么样的函数,每次调用都要malloc出来几kb的空间,而且保存寄存器和恢复也须要必定的性能损失,现代处理器须要保存的寄存器每每有上百字节,仍是比较大的。线程
固然也不是说有栈协程很差,和后面要说的无栈协程相比,有栈协程不须要太多编译器支持,仍是很棒棒的。著名的有栈协程实现包括Windows的Fiber,ucontext,fcontext,boost fiber等。指针
用Windows的Fiber举例,调用起来是这样的
void* main_func; void coro() { int i = 0; i++; SwitchToFiber(main_func); //suspend回main i++; } int main() { main_func = ConvertThreadToFiber(xxx); void* coro = CreateFiber(coro, xxx); SwitchToFiber(coro); //调用coro //coro suspend SwitchToFiber(coro); //让他resume DeleteFiber(coro); }
除了有栈协程,另外一个常见的实现就是无栈协程(stackless coroutine),这是怎么实现的呢?无栈协程须要编译器的转换工做,对于一个简单的协程(伪代码)
void fun() { int i = 0; i++; SUSPEND(); i++; return; }
编译器会将他转写成一个对象(或相似物),以suspend的地方为分界,将函数拆成几部分,每一个部分为一个单独的函数,而后将局部变量都作成类成员,这样每一个被拆出来的子过程均可以访问这个成为了成员变量的局部变量,最后,保存一个状态变量,生成一个MoveNext函数(名字只是为了表述方便),每次调用MoveNext,根据状态变量的值,来执行前面被拆出来的不一样的函数,好比上面的伪代码,会被编译器转写成如下的样子(命名只是为了表述方便)
struct fun_coroutine { int i; int __state = 0; void MoveNext() { switch(__state) { case 0: return __part0(); case 1: return __part1(); } } void __part0() { i = 0; i++; __state = 1; } void __part1() { i++; } };
调用者对fun的调用也会被转写成构造fun_coroutine,而后调用MoveNext成员函数,此时执行的是__part0,__part0的返回就是函数第一次suspend,调用者能够选择第二次调用MoveNext,这时被执行的就是__part1函数了。
由此看来,无栈协程的调用消耗的空间就是局部变量占用的所有空间,相比有栈协程每次分配几KB小不少,并且,无栈协程对象直接构造在调用者的栈上,意味着其中的成员(局部变量)也都在调用者的栈上,相比有栈协程把局部变量放在新开的空间上,CPU cache局部性更好,同时无栈协程的中断和函数返回几乎没有区别,而有栈携程的中断须要保存上百字节的寄存器,而且,无栈携程须要编译器参与,那么编译器彻底能够进行相似函数内联,常量折叠之类的操做,将协程的调用尽量优化到没有。综合性能会比有栈协程更好。
吹了这么多,有栈协程好是好,但是须要编译器支持,而对于C++这种巨复杂的语言,你加点什么东西一要提防着不要影响其它feature和已有代码,二要地方这些东西能不能和已有feature结合,不能冲突,三还要不能限定实现(好比C#的yield只能返回IEnumerator<T>),因此牙膏挤到了C++20甚至23,尚未正式确立加入语言。
我在说无栈协程suspend的性能和函数返回没区别的地方,确定有人会反驳我说fun_coroutine会被new出来啊,异常之类的东西会增大overhead啊,实际上这些东西普通函数也有,你new了一个对象而后调用成员函数,和调用全局函数,区别大吗,我感受是不大。此外有人会怀疑转写成对象+状态会让编译器无法优化,实际上,llvm是直接支持coroutine的,若是你的语言编译到llvm,你的前端能够不把他转换成对象给llvm看,而是直接用llvm的协程原语,剩下的丢给llvm去优化。