协程的概念
C++20引入了协程(coroutine)的概念,首先介绍下什么是协程: 协程是一个可以暂停执行以便稍后恢复的函数。但与函数不同的是协程是无堆栈的:它们通过返回调用者(caller)来暂停执行,恢复执行所需的数据与堆栈分开存储。这允许异步执行顺序代码(例如,在没有显式回调的情况下处理非阻塞I/O),并且还支持延迟计算无限序列和其他用途的算法。
也就是说,通常一个函数的函数体是顺序执行的,只有执行完才会将结果返回给调用者,而对于无栈的协程,可以把函数挂起,然后在任意需要的时刻恢复并执行函数体。
协程的主要优点
协程可以说是进程和线程的升级版,进程和线程切换都会有内核态和用户态的切换,而协程则是用户自己控制切换的时机,并不需要切换至内核态,所以这样以来执行效率非常高。因为程序的切换没有线程切换的开销,并且由于只有一个线程,也不存在同时读写变量的冲突,在协程中控制共享资源不需要加锁,只需要判断数据的状态,故执行效率远高于线程。特别是针对多核的情况,可以使用多进程+协程的方式尽可能提高效率。
协程的使用
一个函数如果想成为一个协程,可通过使用co_return,co_await或者co_yield这三个关键自来实现。
官方实例
官方实例,要求g++版本>=10.0,需要支持C++20。
#include <coroutine>
#include <iostream>
struct promise;
struct coroutine : std::coroutine_handle<promise>
{
using promise_type = ::promise;
};
struct promise
{
coroutine get_return_object() { return {coroutine::from_promise(*this)}; }
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
struct S
{
int i;
coroutine f()
{
std::cout << i;
co_return;
}
};
void bad1()
{
coroutine h = S{0}.f();
// S{0} destroyed
h.resume(); // resumed coroutine executes std::cout << i, uses S::i after free
h.destroy();
}
coroutine bad2()
{
S s{0};
return s.f(); // returned coroutine can't be resumed without committing use after free
}
void bad3()
{
coroutine h = [i = 0]() -> coroutine // a lambda that's also a coroutine
{
std::cout << i;
co_return;
}(); // immediately invoked
// lambda destroyed
h.resume(); // uses (anonymous lambda type)::i after free
h.destroy();
}
void good()
{
coroutine h = [](int i) -> coroutine // make i a coroutine parameter
{
std::cout << i;
co_return;
}(0);
// lambda destroyed
h.resume(); // no problem, i has been copied to the coroutine
// frame as a by-value parameter
h.destroy();
}
int main() {
bad1();
bad2();
bad3();
good();
return 0;
}
编译指令:
g++ co_await.cc -o co_await -fcoroutines
co_await
下面通过实例具体介绍下co_await的使用,通过co_await实现python生成器的功能,代码如下:
#include <concepts>
#include <coroutine>
#include <exception>
#include <iostream>
// 注意这里使用了嵌套类,但不是必须的,
// 也可以将promise_type定义为顶层类,
// 在task内部使用using promise_type = xxx;
struct task_t {
/*
promise_type 用于定义一类协程的行为,包括协程创建方式、协程初始化完成和
结束时的行为、发生异常时的行为、如何生成 awaiter 的行为以及 co_return
的行为等等。promise 对象可以用于记录/存储一个协程实例的状态。每个协程桢与
每个 promise 对象以及每个协程实例是一一对应的。
promise_type里的接口需要我们实现,promise_type里的接口是给编译器调用的。
*/
struct promise_type {
// 返回给 caller 一个对象。
task_t get_return_object() { return {}; }
// 控制协程初始化完成后是否挂起。
// std::suspend_never 是一个空类,它可以用来表示await表达式永远不会挂起,也不会产生值。
std::suspend_never initial_suspend() { return {}; }
// 控制协程执行完后是否挂起。
std::suspend_never final_suspend() noexcept { return {}; }
// 处理异常
void unhandled_exception() {}
};
};
// awaitable是一个await-expression,
// 例如: co_await awaitable表示表示暂停当前协程的运行
// 转而等待awaitable的结果,最后返回一个awaiter
// 告诉co_await要做什么。
struct awaitable {
std::coroutine_handle<>* anchor;
// await_ready告诉co_await准备好没有,如果返回false,
// 如果await_suspend返回coroutine_handle的一个实例h,
// 那么恢复这个handle,即运行await_suspend(h).resume()
// 即暂停本协程同时恢复另一个协程。
// 如果await_suspend返回bool,如果为false则恢复自己。
// 如果await_suspend返回void, 那么直接执行,执行完暂停本协程。
// 如果await_ready返回true或者协程被恢复了,那么执行await_resume,
// 执行完暂停本协程。
// 总结下:
// await_ready:准备好了没有。
// await_suspend:停不停止。
// await_resume:好了做什么。
constexpr bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) {
if (anchor) {
*anchor = h;
anchor = nullptr;
}
}
constexpr void await_resume() const noexcept {}
};
// coroutine_handle用于引用挂起或正在执行的协程,其模板参数是
// promise_type。
task_t counter(std::coroutine_handle<>* continuation, int start) {
awaitable a{continuation};
for (unsigned i = start;; ++i) {
co_await a;
std::cout << i << std::endl;
}
}
struct generator_counter {
int start;
std::coroutine_handle<> closure_func;
generator_counter(int begin) : start{begin}, closure_func{} {
counter(&closure_func, start);
}
void next(){
closure_func();
}
~generator_counter(){
closure_func.destroy();
}
};
int main() {
generator_counter count_from_10(10);
for (int i = 0; i < 5; ++i) {
count_from_10.next();
}
return 0;
}
执行结果如下:
10
11
12
13
14
先说到这里,后续会进一步详细作解释,并进一步介绍co_return和co_yield的功能。