协程 Coroutines
概述
维基百科的定义
协程是计算机程序的⼀类组件,推⼴了协作式多任务的⼦程序,允许执⾏被挂起与被恢复。相对⼦例程⽽⾔,协程更为⼀般和灵活。
yield 在 Python 里叫做“生成器”(generator),返回的是一个可迭代的对象,每次迭代就能得到一个 yield 出来的结果,是一种很常见的协程形式。
from itertools import islice, takewhile
def f():
a = 0
b = 1
while True:
yield b
a, b = b, a + b
# 1 1 2 3 5
for i in islice(f(), 5):
print(i)
# 1 1 2 3 5 8
for i in takewhile(lambda x: x < 10, f()):
print(i)
在代码的执行过程中,f 和它的调用代码是交叉执行的。
a = 0 # f()
b = 0 # f()
yield b # f()
print(i) # 调用者
a, b = 1, 0 + 1 # f()
yield b # f()
print(i) # 调用者
a, b = 1, 1 + 1 # f()
yield b # f()
print(i) # 调用者
a, b = 2, 1 + 2 # f()
yield b # f()
print(i) # 调用者
C++20 协程
C++20 协程的基础是微软提出的 Coroutines TS,在 2019 年 7 月被批准加入到 C++20 草案中。MSVC 和 Clang 已经支持协程。目前被标准化的只是协程的底层语言支持,而不是上层的高级封装。
协程可以有很多不同的用途:
- 生成器
- 异步 I/O
- 惰性求值
- 事件驱动应用
为了正确运行代码,在 VS2019 中使用最新的C++支持 /std:c++ latest
。
协程相关的新关键字:
- co_await
- co_yield
- co_return
这三个关键字最初是没有 co_ 前缀的,但考虑到 await、yield 已经在很多代码里出现,就改成了目前这个样子。同时,return 和 co_return 也作出了明确的区分:一个协程里只能使用 co_return,不能使用 return。这三个关键字只要有一个出现在函数中,这个函数就是一个协程了。
co_await
auto result = co_await expression;
编译器会把它理解为:
auto&& _a = expression;
if (!_a.await_ready()) {
_a.await_suspend(coroutine handle);
// 挂起/恢复点
}
auto result = _a.await_resume();
expression 需要支持 await_ready、await_suspend 和 await_resume 三个接口。
如果 await_ready() 返回真,就代表不需要真正挂起,直接返回后面的结果就可以;否则,执行 await_suspend 之后即挂起协程,等待协程被唤醒之后再返回 await_resume() 的结果。这样一个表达式被称作是个 awaitable。
标准里定义了两个 awaitable:
struct suspend_always
{
bool await_ready() const noexcept { return false; }
void await_suspend(coroutine_handle<>) const noexcept {}
void await_resume() const noexcept {}
};
struct suspend_never
{
bool await_ready() const noexcept { return true; }
void await_suspend(coroutine_handle<>) const noexcept {}
void await_resume() const noexcept {}
};
suspend_always 永远告诉调用者需要挂起,而 suspend_never 则永远告诉调用者不需要挂起。
两者的 await_suspend 和 await_resume 都是平凡实现,不做任何实际的事情。
一个 awaitable 可以自行实现这些接口,以定制挂起之前和恢复之后需要执行的操作。
coroutine_handle 是 C++ 标准库提供的类模板,这个类是用户代码跟系统协程调度真正交互的地方。
- destroy:销毁协程
- done:判断协程是否已经执行完成
- resume:让协程恢复执行
- promise:获得协程相关的 promise 对象,是协程和调用者的主要交互对象,一般类型名称为 promise_type
- from_promise(静态):通过 promise 对象的引用来生成一个协程句柄
协程的执行过程
- 为协程调用分配一个协程帧,含协程调用的参数、变量、状态、promise 对象等所需的空间。
- 调用 promise.get_return_object(),返回值会在协程第一次挂起时返回给协程的调用者。
- 执行 co_await promise.initial_suspsend();根据上面对 co_await 语义的描述,协程可能在此第一次挂起(但也可能此时不挂起,在后面的协程体执行过程中挂起)。
- 执行协程体中的语句,中间可能有挂起和恢复;如果期间发生异常没有在协程体中处理,则调用 promise.unhandled_exception()。
- 当协程执行到底,或者执行到 co_return 语句时,会根据是否有非 void 的返回值,调用 promise.return_value(…) 或 promise.return_void(),然后执行 co_await promise.final_suspsend()。
代码表示如下:
frame = operator new(...);
promise_type& promise = frame->promise;
// 在初次挂起时返回给调用者
auto return_value = promise.get_return_object();
co_await promise.initial_suspsend();
try {
执行协程体;
可能被 co_wait、co_yield 挂起;
恢复后继续执行,直到 co_return;
}
catch (...) {
promise.unhandled_exception();
}
final_suspend:co_await promise.final_suspsend();
co_yield 表达式等价于:
co_await promise.yield_value(expression);
uint64_resumable
#include <coroutine>
using std::coroutine_handle;
// using std::suspend_always;
// using std::suspend_never;
struct suspend_always
{
bool await_ready() const noexcept { return false; }
void await_suspend( coroutine_handle<>) const noexcept {}
void await_resume() const noexcept {}
};
class uint64_resumable {
public:
// 存放协程定制点的类型
struct promise_type
{
// 存放供 uint64_resumable::get 取用的数值。
uint64_t value_;
using coro_handle = coroutine_handle<promise_type>;
// 第一个定制点,调用协程的返回值就是 get_return_object() 的结果。
// 使用 promise 对象来构造一个 uint64_resumable。
auto get_return_object() { return uint64_resumable{coro_handle::from_promise(*this) };}
// 第二个定制点,返回 suspend_always()。
// 协程立即挂起,调用者马上得到 get_return_object() 的结果。
constexpr auto initial_suspend() { return suspend_always(); }
// 第三个定制点,返回 suspend_always(),即使执行到了 co_return 语句,协程仍处于挂起状态。
// 如果返回 suspend_never(),一旦执行了 co_return 或执行到协程结束,
// 协程就会被销毁,连同已初始化的本地变量和 promise,并释放协程帧内存。
constexpr auto final_suspend() { return suspend_always(); }
// 第四个定制点,仅对 value_ 进行赋值,然后让协程挂起(执行控制回到调用者)。
auto yield_value(uint64_t value) { value_ = value; return suspend_always(); }
// 第五个定制点,代码永不返回,无事可做。
void return_void() {}
// 第六个定制点,不应该发生任何异常,调用 terminate 来终结程序的执行。
void unhandled_exception() { std::terminate(); }
};
using coro_handle = coroutine_handle<promise_type>;
// 协程构造需要一个协程句柄。
explicit uint64_resumable( coro_handle handle) : handle_(handle) { }
// 析构时将使用协程句柄来销毁协程。
~uint64_resumable() { handle_.destroy(); }
// 不可复制,以免重复调用 handle_.destroy()。
uint64_resumable( const uint64_resumable&) = delete;
// 允许移动。
uint64_resumable( uint64_resumable&&) = default;
// 恢复执行。
bool resume()
{
if (!handle_.done()) handle_.resume();
return !handle_.done();
}
// 获取数据。
uint64_t get() { return handle_.promise().value_; }
private:
// 协程句柄
coro_handle handle_;
};
测试
uint64_resumable f()
{
uint64_t a = 0;
uint64_t b = 1;
while (true) {
co_yield b;
auto tmp = a;
a = b;
b += tmp;
}
}
调用代码
auto res = f();
while (res.resume())
{
auto i = res.get();
if (i >= 10) break;
cout << i << endl; //1 1 2 3 5 8
}
使用 Coroutines TS 的生成器:
#include <experimental/generator>
using std::experimental::generator;
generator<uint64_t> f()
{
uint64_t a = 0;
uint64_t b = 1;
while (true) {
co_yield b;
auto tmp = a;
a = b;
b += tmp;
}
}
调用代码:
for (auto i : f()) {
if (i >= 10) {
break;
}
cout << i << endl;
}
一些问题
- 操作系统看到的是线程,调度的也是线程。一个线程中可以有很多个协程,这些协程的执行顺序由程序员自己来调度。比较明显的好处:
- 同一个线程中的协程不需要考虑数据的竞争问题,因为这些协程的执行顺序是固定的。
- 协程能够很方便的保存执行状态,使复杂状态机的实现变得简单。
- 最基本协程和线程的区别就是,协程是程序自己调度的,线程是操作系统调度的。操作系统干的事情,开销自然大一些。协程的优势,性能就是很重要的一块。
- 协程在哪个线程运行,是你的代码决定的。你从哪个线程去调用协程或去 co_await、co_yield 了,协程就在那个线程执行。生成器那样的用法,完全只有一个线程,也不适合用多线程来改造。
- 本文链接:https://morisa66.github.io/2021/02/15/Coroutines/
- 版权声明:本博客所有文章除特别声明外,均默认采用 许可协议。