C++ 20 协程总结
介绍
C++ 20提供的是非对称的、一等对象、无栈的协程(Coroutines in C++20 are asymmetric, first-class, and stackless)
所谓协程,即用户级线程,一种用于将异步代码同步化的编程机制,使得程序的执行流可以在多个并行事务之间切换但又不必承担切换带来的过高的性能损耗。当前很多的编程语言都内置协程特性或者有自己的协程库,如C/C++的libco、golang的goroutine等。而在实现机制上,又可以划分为有栈协程和无栈协程
协程是可以在保持状态的同时暂停和恢复执行的函数
非对称协程与对称协程
非对称协程(asymmetric coroutines)是跟一个特定的调用者绑定的,协程让出CPU时,只能让回给原调用者。那到底是什么东西“不对称”呢?第一,非对称在于程序控制流转移到被调协程时使用的是suspend/resume操作,而当被调协程让出 CPU 时使用的却是return/yield操作。第二,协程间的地位也不对等,caller与callee关系是确定的,不可更改的,非对称协程只能返回最初调用它的协程。微信团队的libco其实就是一种非对称协程,Boost C++库也提供了非对称协程。另外,挂起(suspend)和恢复(resume)跟yield的区别是:yield后的协程,之后还会被切换回来,但是被suspend挂起的协程,除非调用resume()恢复它,否则永远不会再被执行到。在不同语言中,这三者会有不同的叫法,比如call也会调用新函数时也会同时实现suspend旧函数的功能,有的语言用yield/resume和return,不一而论,但区别不变。
对称协程(symmetric coroutines)则不同,被调协程启动之后就跟之前运行的协程没有任何关系了。协程的切换操作,一般而言只有一个操作yield或return,用于将程序控制流转移给另外的协程。对称协程机制一般需要一个调度器的支持,按一定调度算法去选择yield或return的目标协程。Go语言提供的协程,其实就是典型的对称协程。不但对称,goroutines还可以在多个线程上迁移。这种协程跟操作系统中的线程非常相似,甚至可以叫做“用户级线程”。
一等对象(第一类对象)
[python - What are “first-class” objects? - Stack Overflow](https://stackoverflow.com/questions/245192/what-are-first-class-objects
第一类对象(英语:First-class object)在计算机科学中指可以在执行期创造并作为参数传递给其他函数或存入一个变量的实体[1]。将一个实体变为第一类对象的过程叫做“物件化”(Reification)[2]。
无栈协程
有栈(stackful)协程通常的实现手段是在堆上提前分配一块较大的内存空间(比如 64K),也就是协程所谓的“栈”,参数、return address 等都可以存放在这个“栈”空间上。如果需要协程切换,那么通过 swapcontext 一类的形式来让系统认为这个堆上空间就是普通的栈,这就实现了上下文的切换。
有栈协程最大的优势就是侵入性小,使用起来非常简便,已有的业务代码几乎不需要做什么修改,但是 C++20 最终还是选择了使用无栈协程,主要出于下面这几个方面的考虑。
无栈协程是一个可以暂停和恢复的函数,是函数调用的泛化。
我们知道一个函数的函数体(function body)是顺序执行的,执行完之后将结果返回给调用者,我们没办法挂起它并稍后恢复它,只能等待它结束。而无栈协程则允许我们把函数挂起,然后在任意需要的时刻去恢复并执行函数体,相比普通函数,协程的函数体可以挂起并在任意时刻恢复执行。
所以,从这个角度来说,无栈协程是普通函数的泛化。
总结一下,有栈协程是用户态线程,无栈协程就是函数调用
设计目标
- 高度可伸缩性
- 高效的恢复和挂起函数操作
- 与已有设施无缝衔接,没有开销
- 允许开发者设计协程库,开放高级语义的接口
- 在禁用异常的环境可以使用
成为协程
一个函数成为一个协程,通过使用以下关键字中的一个
co_return
co_await
co_yield
- 循环中的
co_await
区分协程工厂和协程对象
术语协程通常用于协程的两个方面:一个是调用了co_await
,co_yield
,co_return
的函数,另一个是协程对象
使用一个协程术语形容协程的两个方面会让人糊涂
MyFuture<int> createFuture() {
co_return 2021; }
int main() {
auto fut = createFuture();
std::cout << "fut.get(): " << fut.get() << '\n'; }
函数createFuture
是一个协程工厂返回一个协程对象。协程对象时一个可恢复对象,使用协程框架来指定他的行为
实现协程的框架包含了20多个函数,一些必须实现,一些必须重写,因此你可以定制协程的功能
一个协程与三个部分相关:
promise object
coroutine handle
coroutine frame
通过coroutine handle
协程句柄与promise object
进行交互,并将上下文保存在coroutine frame
编译器在协程执行过程中会自动调用这些函数
协程句柄(coroutine handle
)
协程句柄是一个非拥有的句柄,用于从外部恢复或销毁协程帧(frame)。协程句柄是可恢复函数的一部分。
template <typename T>
struct Generator
{
struct promise_type;
using handle_type = std::coroutine_handle<promise_type>;
Generator(handle_type h): coro(h)
{
}
handle_type coro;
~Generator()
{
if (coro) coro.destroy();
}
T getValue()
{
return coro.promise().current_value;
}
bool next()
{
coro.resume();
return not coro.done();
}
}
- 恢复协程执行:
coro.resume()
- 销毁协程:
coro.destroy()
- 检查状态:
coro
(15行)
协程帧Coroutine Frame
协程帧维持着协程堆内存的分配状态,包含promise_type
,协程复制的参数,挂起点的表示,局部变量等
- 协程的生命周期必须嵌套在调用者的生命周期内
- 协程的调用者知到协程帧的大小
协程帧的关键是可等待体(**Awaitables **),等待器(Awaiters)
可等待体和等待器
promise_type
中的三个函数返回可等待体 yield_value
, initial_suspend
, final_suspend
可等待体
可等待体决定协程是否暂停
本质上,编译器使用promise和co_await操作符生成这三个函数调用。
co_await
需要一个可等待体作为参数
实现可等待体需要三个函数
C++20标准已经定义了两个基本的对象:std::suspend_always
,std::suspend_never
The Awaitable std::suspend_always
struct suspend_always {
constexpr bool await_ready() const noexcept { return false; }
constexpr void await_suspend(std::coroutine_handle<>) const noexcept {}
constexpr void await_resume() const noexcept {}
};
总是挂起,await_ready
返回false
The Awaitable std::suspend_never
struct suspend_never {
constexpr bool await_ready() const noexcept { return true; }
constexpr void await_suspend(std::coroutine_handle<>) const noexcept {}
constexpr void await_resume() const noexcept {}
};
从不挂起,await_ready
返回true
当协程协程执行的时候,这两个函数会自动执行:
- 开始
initial_suspend
- 结束
final_suspend
initial_suspend
当initial_suspend返回suspend_always时,协程会在开始时挂起;返回suspend_never时,则不会挂起
A lazy coroutine
std::suspend_always initial_suspend() {
return {};
}
A eager coroutine
std::suspend_never initial_suspend() {
return {};
}
final_suspend
在协程结束时执行,与几乎initial_suspend相同
可等待体和等待器
- 可被等待的对象称为可等待(awaitable )体或者表达式;
- co_await运算符必须返回一个等待器(awaiter):
- 可等待体和等待器可以是同一个类型;
- std::future(实验)是可等待体。
- co_await运算符返回等待器_Future_awaiter
工作流
编译器执行两个工作流外部的promise
工作流和内部的awaiter
工作流
Promise 工作流
当在函数中使用co_yield, co_await, co_return
,函数成为一个协程,并且编译器将其转换成等价的如下代码
The transformed coroutine
主要工作步骤:
- 协程开始执行:
- 申请必要的协程帧空间
- 拷贝所有函数参数到协程帧
- 创建
promise_type
对象 - 调用
promise_type
中的get_return_object
方法创建协程句柄(coroutine handle
),并保持在局部变量中,当协程第一次挂起时,将返回给调用者 - 调用
initial_suspend
并且co_await
其结果,可能返回suspend_always/never
- 当
co_await prom.initial_suspend
恢复resume
时,函数体执行 - 协程到达挂起点:
- 返回对象(prom.get_return_object())将返回给恢复协程的调用程序
- 协程到达
co_return
- 调用
prom.retrun_void/value
没有返回值或者返回值 - 销毁变量
- 调用
prom.final_suspend()
并且co_await
它的结果 - 协程销毁
- 调用
promise_type
对象和函数参数对象的析构函数 - 释放协程帧的内存
- 返还控制权给调用者
- 调用
co_await
执行等待器工作流
Awaiter工作流
调用co_await
会让编译器执行三个函数:await_ready await_suspend await_resume
The generated Awaiter Workflow
只有await_ready
返回false
时,流程才会执行,否则的话直接返回await_resume
的结果
await_ready
返回false
时:
- 协程挂起,计算
awaitable.await_suspend()
的返回值,返回值有很多种情况
出现异常情况不想写了
co_return
协程使用co_return
作为返回语句
template <typename T>
struct MyFuture
{
std::shared_ptr<T> value;
MyFuture(std::shared_ptr<T> p): value(p)
{
}
~MyFuture()
{
}
T get()
{
return *value;
}
struct promise_type
{
std::shared_ptr<T> ptr = std::make_shared<T>();
~promise_type()
{
}
MyFuture<T> get_return_object() { return ptr; }
void return_value(T v) { *ptr = v; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void unhandled_exception()
{
std::terminate();
}
};
};
MyFuture<int> createFuture()
{
co_return 2021;
}
int main(int argc, char* argv[])
{
auto fut = createFuture();
std::cout << fut.get() << std::endl;
}
- 流程
- 初始化协程
- 申请必要的协程帧空间
- 拷贝所有函数参数到协程帧
- 创建
promise_type
对象 - 调用
promise_type
中的get_return_object
方法将ptr
传给fut
- 调用
co_return
- 调用
return_value
并传入参数2022 - 输出
fut.get()
co_yield
无限数据流
template <typename T>
struct Generator
{
struct promise_type;
using handle_type = std::coroutine_handle<promise_type>;
Generator(handle_type h): coro(h)
{
}
handle_type coro;
~Generator() { if (coro) coro.destroy(); }
Generator(const Generator&) = delete;
Generator& operator =(const Generator&) = delete;
Generator(Generator&& oth) noexcept : coro(oth.coro)
{
oth.coro = nullptr;
}
Generator& operator =(Generator&& oth) noexcept
{
coro = oth.coro;
oth.coro = nullptr;
return *this;
}
T getValue()
{
return coro.promise().current_value;
}
bool next()
{
coro.resume();
return !coro.done();
}
struct promise_type
{
promise_type() = default;
~promise_type() = default;
auto initial_suspend()
{
return std::suspend_always{};
}
auto final_suspend() noexcept
{
return std::suspend_always{};
}
auto get_return_object()
{
return Generator{handle_type::from_promise(*this)};
}
auto return_void()
{
return std::suspend_never{};
}
auto yield_value(const T value)
{
current_value = value;
return std::suspend_always{};
}
void unhandled_exception()
{
std::terminate();
}
T current_value;
};
};
Generator<int> getNext(int start = 0, int step = 1)
{
auto value = start;
while (true)
{
co_yield value;
value += step;
}
}
int main(int argc, char* argv[])
{
auto gen = getNext();
for (int i = 0; i <= 10; ++i)
{
gen.next();
std::cout << std::format("gen value: {}\n", gen.getValue());
}
std::cout << "\n\n";
auto gen2 = getNext(100, -10);
for (int i = 0; i <= 20; ++i)
{
gen2.next();
std::cout << std::format("gen2 value: {}\n", gen2.getValue());
}
}
看一下执行流程;
- 创建
promise_type
- 调用
get_return_object()
,将其结果保存在局部变量 - 创建
generator
- 调用
initial_suspend
挂起协程 - 请求下一个值并消耗一个值然后挂起
- 接着调用
gen.next
重复循环
co_await
struct Job
{
struct promise_type;
using handle_type = std::coroutine_handle<promise_type>;
handle_type coro;
Job(handle_type h): coro(h)
{
}
~Job()
{
if (coro) coro.destroy();
}
void start()
{
coro.resume();
}
struct promise_type
{
auto get_return_object()
{
return Job{handle_type::from_promise(*this)};
}
std::suspend_always initial_suspend()
{
std::cout << "准备工作\n";
return {};
}
std::suspend_always final_suspend() noexcept
{
std::cout << "执行工作\n";
return {};
}
void return_void()
{
}
void unhandled_exception()
{
}
};
};
Job prepareJob()
{
co_await std::suspend_never();
}
int main(int argc, char* argv[])
{
std::cout << "工作之前\n";
auto job = prepareJob();
job.start();
std::cout << "工作之后\n";
}