C++ 20有一个新玩意,协程。这玩意对C++的未来可能是重要一环,也会是让C++成为服务器编程有力工具。
对C++20的协程,最简单的理解协程是可以重入的特殊函数。就是这个函数在执行的过程,可以(通过co_await ,或者co_yield)挂起,然后在外部(通过coroutine_handle)恢复运行。
我测试的代码都是在Visual studio 2022上运行的。据说GCC 10.0 也已经支持。
首先再次强调,C++ 20的协程是一个特殊函数。只是这个函数具有挂起和恢复的能力,可以被挂起(挂起后调用代码继续向后执行),而后可以继续恢复其执行。如下图:
如图所示,协程并没有一次执行完成,可以被反复挂起,挂起后可以恢复到挂起的点继续运行。
那我们来看看C++ 20 协程的一些特点和用途。
首先,C++ 20 协程是一个无栈(stackless)的协程。同时,C++ 20 协程是非对称的协程,和Linux传统的Context Switch有区别。更像Windows的纤程。和C#的协程也比较相像,毕竟是微软的提案。
传统的Context Switch是有栈协程,你可以认为Context 协程都是运行在栈上,Context 协程的切换就是切换栈。同时因为其是有栈协程。切换是对称的,都是栈切换。你可以从主线程上切换为另外一个Context 协程栈,也可以从一个Context 协程切换为主线程,也可以Context 协程之间切换。Context 协程的状态也就是保存在栈上。
C++ 20的协程可以用来干啥呢?和大部分协程用途类似,就是异步编程用的。看图1就可以明白,每次一次协程的挂起都可以视为协程进入一个等待状态,比如请求一个网络,需要HTTP get一个文件,然后对文件进行分析。那么就可以用协程来包装整个处理,在发起HTTP请求后,挂起协程(处理其他事情),等待应答或者超时后,再恢复协程的运行。
但不足的是目前C++ 20的协程才是一个开始,说实话,目前的协程只提供基本框架,写起来并不舒服。C++目前在IO方面,特别是网络IO方面还不完善。需要一个大量异步IO库,才能用好C++ 20协程。
如果C++ 20的协程周边更加完整,也许C++又能在服务器编程这块能重新面对Go这类语言的威胁。
C++的协程(协程函数)内部可以用co_await , co_yield.两个关键字挂起协程,co_return,关键字进行返回。
co_await
co_await调用一个awaiter对象(可以认为是一个接口),根据其内部定义决定其操作是挂起,还是继续,以及挂起,恢复时的行为。其呈现形式为
cw_ret = co_await awaiter;
cw_ret 记录调用的返回值,其是awaiter的await_resume 接口返回值。
co_await 相对比较复杂,后面开一章详细讲。
co_yield
挂起协程。其出现形式是
co_yield cy_ret;
cy_ret会保存在promise承诺对象中(通过yield_value函数)。在协程外部可以通过promise得到。
co_return
协程返回。其出现形式是
co_return cr_ret;
cr_ret会保存在promise承诺对象中(通过return_value函数)。在协程外部可以通过promise得到。要注意,cr_ret并不是协程的返回值。这个是有区别的。
C++ 的编译器如何识别协程函数呢?是通过函数返回值。C++ 协程函数的返回类型有要求,返回类型是result ,而result里面必须有一个子类型承诺对象(promise),呈现为Result::promise_type。承诺对象(promise)是一个接口,里面实现get_return_object等接口。而通过std::coroutine_handle<promise_type>::from_promise( promise& p )这个静态函数,我们可以得到协程句柄(coroutine handle)。而协程的运行状态 ,协程函数的形参,内部变量,临时变量,挂起暂停在什么点,被保存在协程状态 (coroutine state)中。
好了上面的描述,我们可以看出协程的几个重要概念。
承诺对象
形参(协程函数的参数)
协程挂起的点
临时变量
协程状态(coroutine state)
协程状态(coroutine state)是协程启动开始时,new空间存放协程状态,协程状态记录协程函数的参数,协程的运行状态,变量。挂起时的断点。
注意,协程状态 (coroutine state)并不是就是协程函数的返回值RET。虽然我们设计的RET一般里面也有promise和coroutine handle,大家一般也是通过RET去操作协程的恢复,获取返回值。但coroutine state理论上还应该包含协程运行参数,断点等信息。而协程状态 (coroutine state)应该是协程句柄(coroutine handle)对应的一个数据,而由系统管理的。
承诺对象(promise)
承诺对象的表现形式必须是result::promise_type,result为协程函数的返回值。
承诺对象是一个实现若干接口,用于辅助协程,构造协程函数返回值;提交传递co_yield,co_return的返回值。明确协程启动阶段是否立即挂起;以及协程内部发生异常时的处理方式。其接口包括:
前面我们提到在协程创建的时候,会new协程状态(coroutine state)。你可以通过可以在 promise_type 中重载 operator new 和 operator delete,使用自己的内存分配接口。(请参考再探 C++20 协程)
协程句柄(coroutine handle)
协程句柄(coroutine handle)是一个协程的标示,用于操作协程恢复,销毁的句柄。
协程句柄的表现形式是std::coroutine_handle<promise_type>,其模板参数为承诺对象(promise)类型。句柄有几个重要函数:
协程句柄和承诺对象之间是可以相互转化的。
co_wait 关键字会调用一个等待体对象(awaiter)。这个对象内部也有3个接口。根据接口co_wait 决定进行什么操作。
void 同返回true
bool 返回true 立即挂起,返回false 不挂起。
返回某个协程句柄(coroutine handle),立即恢复对应句柄的运行。
等待体(awaiter)值得用更加详细的笔墨书写一章,我们就放一下,先了解其有2个特化类型。
前面不少接口已经用了这2个特化的类,同时也可以明白其实协程内部不少地方其实也在使用co_wait 关键字。
好了。所有概念我们介绍基本完成了。先来段代码吧。否则实在憋屈。
这个例子主要展现的是协程函数和主线程之间的切换。协程反复中断,然后在main函数内部又恢复其运行。直至最后co_return。
这个例子虽然简单,但如果你对异步编程有所了解也能明白如何利用C++20完成一段异步编程了。源代码获取地址请点击
下面例子中:
#include <coroutine>
#include <iostream>
#include <stdexcept>
#include <thread>
//!coro_ret 协程函数的返回值,内部定义promise_type,承诺对象
template <typename T>
struct coro_ret
{
struct promise_type;
using handle_type = std::coroutine_handle<promise_type>;
//! 协程句柄
handle_type coro_handle_;
coro_ret(handle_type h)
: coro_handle_(h)
{
}
coro_ret(const coro_ret&) = delete;
coro_ret(coro_ret&& s)
: coro_handle_(s.coro_)
{
s.coro_handle_ = nullptr;
}
~coro_ret()
{
//!自行销毁
if (coro_handle_)
coro_handle_.destroy();
}
coro_ret& operator=(const coro_ret&) = delete;
coro_ret& operator=(coro_ret&& s)
{
coro_handle_ = s.coro_handle_;
s.coro_handle_ = nullptr;
return *this;
}
//!恢复协程,返回是否结束
bool move_next()
{
coro_handle_.resume();
return coro_handle_.done();
}
//!通过promise获取数据,返回值
T get()
{
return coro_handle_.promise().return_data_;
}
//!promise_type就是承诺对象,承诺对象用于协程内外交流
struct promise_type
{
promise_type() = default;
~promise_type() = default;
//!生成协程返回值
auto get_return_object()
{
return coro_ret<T>{handle_type::from_promise(*this)};
}
//! 注意这个函数,返回的就是awaiter
//! 如果返回std::suspend_never{},就不挂起,
//! 返回std::suspend_always{} 挂起
//! 当然你也可以返回其他awaiter
auto initial_suspend()
{
//return std::suspend_never{};
return std::suspend_always{};
}
//!co_return 后这个函数会被调用
void return_value(T v)
{
return_data_ = v;
return;
}
//!
auto yield_value(T v)
{
std::cout << "yield_value invoked." << std::endl;
return_data_ = v;
return std::suspend_always{};
}
//! 在协程最后退出后调用的接口。
//! 若 final_suspend 返回 std::suspend_always 则需要用户自行调用
//! handle.destroy() 进行销毁,但注意final_suspend被调用时协程已经结束
//! 返回std::suspend_always并不会挂起协程(实测 VSC++ 2022)
auto final_suspend() noexcept
{
std::cout << "final_suspend invoked." << std::endl;
return std::suspend_always{};
}
//
void unhandled_exception()
{
std::exit(1);
}
//返回值
T return_data_;
};
};
//这就是一个协程函数
coro_ret<int> coroutine_7in7out()
{
//进入协程看initial_suspend,返回std::suspend_always{};会有一次挂起
std::cout << "Coroutine co_await std::suspend_never" << std::endl;
//co_await std::suspend_never{} 不会挂起
co_await std::suspend_never{};
std::cout << "Coroutine co_await std::suspend_always" << std::endl;
co_await std::suspend_always{};
std::cout << "Coroutine stage 1 ,co_yield" << std::endl;
co_yield 101;
std::cout << "Coroutine stage 2 ,co_yield" << std::endl;
co_yield 202;
std::cout << "Coroutine stage 3 ,co_yield" << std::endl;
co_yield 303;
std::cout << "Coroutine stage end, co_return" << std::endl;
co_return 808;
}
int main(int argc, char* argv[])
{
bool done = false;
std::cout << "Start coroutine_7in7out ()\n";
//调用协程,得到返回值c_r,后面使用这个返回值来管理协程。
auto c_r = coroutine_7in7out();
//第一次停止因为initial_suspend 返回的是suspend_always
//此时没有进入Stage 1
std::cout << "Coroutine " << (done ? "is done " : "isn't done ")
<< "ret =" << c_r.get() << std::endl;
done = c_r.move_next();
//此时是,co_await std::suspend_always{}
std::cout << "Coroutine " << (done ? "is done " : "isn't done ")
<< "ret =" << c_r.get() << std::endl;
done = c_r.move_next();
//此时打印Stage 1
std::cout << "Coroutine " << (done ? "is done " : "isn't done ")
<< "ret =" << c_r.get() << std::endl;
done = c_r.move_next();
std::cout << "Coroutine " << (done ? "is done " : "isn't done ")
<< "ret =" << c_r.get() << std::endl;
done = c_r.move_next();
std::cout << "Coroutine " << (done ? "is done " : "isn't done ")
<< "ret =" << c_r.get() << std::endl;
done = c_r.move_next();
std::cout << "Coroutine " << (done ? "is done " : "isn't done ")
<< "ret =" << c_r.get() << std::endl;
return 0;
}
C++ 20 的协程是一个用于异步模型,主要用于服务开发的东东,由于概念和传统的同步变成有区别,同时自身的概念,接口,关键字很多。理解起来有点难度。
如果你对协程的各种接口调用顺序有疑惑,可以自己调试一下,比看文字记忆深刻。如果您没有耐心也可以看看第三章的文字。
初探 C++20 协程
再探 C++20 协程,这两篇文字都不错。
Coroutines (C++20)
协程(coroutine)简介
The Coroutine in C++ 20 协程之诺