C++20 特性 协程 Coroutines(1)
作者:孙孟越
我们先来介绍一下什么是协程.
一、协程简单介绍
协程和普通的函数 其实差不多. 不过这个 "函数" 能够暂停自己, 也能够被别人恢复.
普通的函数调用, 函数运行完返回一个值, 结束.
协程可以运行到一半, 返回一个值, 并且保留上下文. 下次恢复的时候还可以接着运行, 上下文 (比如局部变量) 都还在.
这就是最大的区别.
二、协程的好处
考虑多任务协作的场景. 如果是线程的并发, 那么大家需要抢 CPU
用, 还需要条件变量/信号量或者上锁等技术, 来确保正确的线程正在工作.
如果在协程中, 大家就可以主动暂停自己, 多个任务互相协作. 这样可能就比大家一起抢 CPU
更高效一点, 因为你能够控制哪个协程用上 CPU
.
一个例子:
生产者/消费者模型: 生产者生产完毕后, 暂停自己, 把控制流还给消费者. 消费者消费完毕后, resume
生产者, 生产者继续生产. 这样循环往复.
异步调用: 比如你要请求网络上的一个资源.
- 发请求给协程
- 协程收到请求以后, 发出请求. 协程暂停自己, 把控制权还回去.
- 你继续做些别的事情. 比如发出下一个请求. 或者做一些计算.
- 恢复这个协程, 拿到资源 (可能还要再等一等)
理想状态下, 4 可以直接用上资源, 这样就完全不浪费时间.
如果是同步的话:
- 发请求给函数.
- 函数收到请求以后, 等资源.
- 等了很久, 资源到了, 把控制权还回去.
明显需要多等待一会儿. 如果需要发送上百个请求, 那显然是第一种异步调用快一点. (等待的过程中可以发送新的请求)
如果没有协程的话, 解决方案之一是使用多线程. 像这样:
- 发请求给函数.
- 函数在另外的线程等, 不阻塞你的线程.
- 你继续做些别的事情. 比如发出下一个请求. 或者做一些计算.
- 等到终于等到了, 他再想一些办法通知你.
然后通知的办法就有 promise
和回调这些办法.
三、协程得用法
我们照着 C++20 标准来看看怎么用协程. 用 g++, 版本 10.2 进行测试.
目前 C++20 标准只加入了协程的基本功能, 还没有直接能上手用的类. GCC 说会尽量与 clang
和 MSVC
保持协程的 ABI 兼容, 同时和 libc++ 等保持库的兼容. 所以本文可能也适用于它们.
协程和主程序之间通过 promise
进行通信. promise
可以理解成一个管道, 协程和其调用方都能看得到.
以前的 std::async
和 std::future
也是基于一种特殊的 promise
进行通信的, 就是 std::promise
. 如果要使用协程, 则需要自己实现一个全新的 promise
类, 原理上是类似的.
四、协程三个关键字
这次引入了三个新的关键字 co_await
, co_yield
, co_return
. 从效果上看: co_await 是用来暂停和恢复协程的, 并且真正用来求值.
co_yield 是用来暂停协程并且往绑定的 promise
里面 yield
一个值.
co_return 是往绑定的 promise
里面放入一个值.
这里我们先谈谈 co_yield
和 co_return.
谈完这俩再谈谈 co_await
就比较简单.
五、协程工作原理
所以最重要的两个问题就是
- 协程如何实现信息传递 (使用自己实现的
promise
) - 如何恢复一个已经暂停了的协程 (使用
std::coroutine_handle
)
上面说了, 一个协程会有一个与之相伴的 promise
, 用作信息传递. 一个协程, 效果等同于
{ promise-type promise(promise-constructor-arguments); try { co_await promise.initial_suspend(); // 创建之后 第一次暂停 function-body // 函数体 } catch ( ... ) { if (!initial-await-resume-called) throw; promise.unhandled_exception(); } final-suspend: co_await promise.final_suspend(); // 最后一次暂停 }
细节, 包括 promise
初始化的参数, 异常的处理等等, 我们留到之后的文章再处理. 所以我们简化成
{ promise-type promise; co_await promise.initial_suspend(); function-body // 函数体 final-suspend: co_await promise.final_suspend(); }
对于暂停, co_await
那个地方就可以暂停并且交出控制权. 下篇文章我们会详细介绍 co_await
.
对于唤醒, 则需要拿到一个 std::coroutine_handle
, 对它调用 resume()
.
1、co_yield
co_yield 123
做的事情实际上相当于调用了 co_await promise.yield_value(123)
. 这个 promise
里面存放了 123 以后, 会告诉 co_await 自己要暂停. 于是 co_await
就在这里停下来, 把控制流还回去.
来看一个标准中的实现范例.
#include <iostream> #include <coroutine> struct generator { struct promise_type; using handle = std::coroutine_handle<promise_type>; struct promise_type { int current_value; static auto get_return_object_on_allocation_failure() { return generator{nullptr}; } auto get_return_object() { return generator{handle::from_promise(*this)}; } auto initial_suspend() { return std::suspend_always{}; } auto final_suspend() { return std::suspend_always{}; } void unhandled_exception() { std::terminate(); } void return_void() {} auto yield_value(int value) { current_value = value; return std::suspend_always{}; // 这是一个 awaiter 结构, 见第二篇文章 } }; bool move_next() { return coro ? (coro.resume(), !coro.done()) : false; } int current_value() { return coro.promise().current_value; } generator(generator const &) = delete; generator(generator &&rhs) : coro(rhs.coro) { rhs.coro = nullptr; } ~generator() { if (coro) coro.destroy(); } private: generator(handle h) : coro(h) {} handle coro; }; generator f() { co_yield 1; co_yield 2; } int main() { auto g = f(); // 停在 initial_suspend 那里 while (g.move_next()) // 每次调用就停在下一个 co_await 那里 std::cout << g.current_value() << std::endl; }
generator
是一个包装类, 持有一个 std::coroutine_handle
. 同时它规定了 coroutine_handle
本协程的 promise
是什么样的. (通过 generator::promise_type
告知)
coroutine_handle
是协程的流程管理者, 由它来管理这个 promise. 而 generator 则是 coroutine_handle
的管理者.
f() 是一个协程. 可以展开成这样的伪代码
{ generator g(handle coro); // 建立句柄和包装类 co_await promise.initial_suspend(); // 创建之后停在这里, 等待被恢复 co_await promise.yield_value(1); // 第一次恢复后就会停在这里 co_await promise.yield_value(2); // 第二次恢复后就会停在这里 final-suspend: co_await promise.final_suspend(); // 第三次恢复后就会停在这里 }
按照这里的写法, 每一次 promise.yield_value()
之后都会返回一个结构体给 co_await
, 告诉 co_await
自己在这里暂停.
然后在主函数处调用 g.move_next()
, 进而恢复了协程之后, 协程就会从刚刚暂停的 co_await
那一行恢复运行.
对了, 过了最后的 final_suspend()
以后, 这个协程就会析构掉. 再次恢复协程就会导致 segmentation fault
.
g++10 已经提供了协程的支持, 只需要加上 -std=c++20 -fcoroutines -fno-exceptions 即可. 上面这段代码可以在这里编译:
2、co_return
co_return
相当于调用了 promise.return_value()
或者 promise.return_void()
然后跳到 final-suspend
标签那里. 也就是说这个这个协程结束了, 再也无法被恢复了.
而对比 co_yield
调用的是 co_await promise.yield_value().
他们的区别就是 co_yeild
完了协程继续等着下一次被恢复 , co_return
而 co_return
完了协程就结束了. (为了让协程也能像普通函数一样返回)
我们来看一段代码.
#include <iostream> #include <future> #include <coroutine> using namespace std; struct lazy { struct promise_type; using handle = std::coroutine_handle<promise_type>; struct promise_type { int _return_value; static auto get_return_object_on_allocation_failure() { return lazy{nullptr}; } auto get_return_object() { return lazy{handle::from_promise(*this)}; } auto initial_suspend() { return std::suspend_always{}; } auto final_suspend() { return std::suspend_always{}; } void unhandled_exception() { std::terminate(); } void return_value(int value) { _return_value = value; } }; bool calculate() { if (calculated) return true; if (!coro) return false; coro.resume(); if (coro.done()) calculated = true; return calculated; } int get() { return coro.promise()._return_value; } lazy(lazy const &) = delete; lazy(lazy &&rhs) : coro(rhs.coro) { rhs.coro = nullptr; } ~lazy() { if (coro) coro.destroy(); } private: lazy(handle h) : coro(h) {} handle coro; bool calculated{false}; }; lazy f(int n = 0) { co_return n + 1; } int main() { auto g = f(); g.calculate(); // 这时才从 initial_suspend 之中恢复, 所以就叫 lazy 了 cout << g.get(); }
由于这个协程只能被恢复一次, 所以我稍稍修改了一下 lazy 的实现. 可以参考这里:
到此这篇关于C++20 特性 协程 Coroutines
的文章就介绍到这了,更多相关C++20 协程 Coroutines
内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!