C++ 20中采用的是微软的无栈协程实现,保持高度的灵活性和定制性。
有栈(stackful)协程
有栈协程通常的实现是在堆上预先分配一块较大的空间(比如64k),也就是协程的“栈”,参数,返回地址等都会存放在这个栈上。如果需要协程切换,那么通过swap context一类的形式来让系统认为这个堆上的空间就是普通的栈,这样一来就可以实现上下文的切换。
优点:
有栈协程最大的优势就是侵入性小,使用方便。
缺点:
- 栈空间有限
有栈协程的栈空间通常是比较小的,存在栈溢出的风险,但是增大栈大小又会造成空间的浪费;无栈协程避免了这两点。
- 性能
有栈协程相比较普通线程是轻量的,但是相比于无栈协程还是偏重的,目前这一点在实际使用中没有感觉,这是因为一般异步系统都伴随着IO操作,相比于切换开销高几个量级。这也就决定了无栈协程可以用来做一些有意思的操作,C++20 coroutines 提案的作者 Gor Nishanov 在 CppCon 2018 上演示了无栈协程能做到纳秒级的切换,并基于这个特点实现了减少 Cache Miss 的特性。
https://www.youtube.com/watch?v=j9tlJAqMV7U&ab_channel=CppCon
无栈协程
基础概念
协程帧(coroutine frame)
当caller调用一个协程时会先创建一个协程帧,协程帧会构建promise对象,再通过promise对象产生return object。
协程帧主要包含:协程参数,局部变量,promise对象
这些内容在协程恢复运行的时候需要用到,caller通过协程帧的句柄std::coroutine_handle来访问协程帧。
promise_type
promise_type是promise对象的类型。promise_type定义了一类协程行为,包括协程的创建方式、协程初始化完成和结束时的行为、发生异常时的行为、如何生成awaiter的行为以及co_return的行为等。promise对象可以用于记录/存储一个协程实例的状态。每个协程帧与每个promise对象以及每个协程实例是一一对应的。
coroutine return object
它是promise.get_return_object()创建的,一种常见的实现手法会将coroutine_handle存储到coroutine object内,使得该return object获得访问协程的能力。
std::coroutine_handle
协程帧的句柄,主要用于访问底层的协程帧、恢复协程和释放协程。
我们可以通过std::coroutine_handle()唤醒协程。
co_await、awaiter、awaitable
- co_await:一元操作符;
- awaitable:支持co_await操作符的类型;
- awaiter:定义了await_ready、await_suspend和await_resume方法的类型。
co_await expr通常用于表示等待一个任务完成,expr类型需要是一个awaitable,而该表达式的具体语义取决于根据该awaitable生成的awaiter。
协程对象如何协作
Return_t foo () {auto res = co_await awaiter;co_return res ;}
Return_t:promise return object
awaiter: 等待一个task完成。
图中浅蓝色部分的方法就是 Return_t 关联的 promise 对象的函数,浅红色部分就是 co_await 等待的 awaiter。
这个流程的驱动是由编译器根据协程函数生成的代码驱动的,分成三部分:
- 协程创建;
- co_await awaiter 等待 task 完成;
- 获取协程返回值和释放协程帧。
foo()协程会生成下面这样的模板代码(伪代码),协程的创建都会产生类似的代码:
{co_await promise.initial_suspend();try{coroutine body;}catch (...){promise.unhandled_exception();}FinalSuspend:co_await promise.final_suspend();}
首先需要创建协程,创建协程之后是否挂起则由调用者设置 initial_suspend 的返回类型来确定。
创建协程的流程大概如下:
- 创建一个协程帧(coroutine frame)
- 在协程帧里构建 promise 对象
- 把协程的参数拷贝到协程帧里
