概述
所谓“协程”,简单点说就是一个函数可以中止运行,待“条件成熟”后,再次在断点的地方重新运行,即能以“进入-暂停-继续-暂停…”这样的方式运行函数,因此讨论协程,需要关心下面几个问题:
- 在运行点暂停,然后再运行—暂停点的记录
- 暂停时的运行环境,恢复运行时,应首先恢复当时的环境—运行环境的记录
- 因协程状态是变化的,因此需要有办法查询协程当前的运行状态,包括返回值、异常等—协程状态的记录
异步操作是 “慢”资源与“快”资源协调编程方面的一个“老”问题,协程的目标就是试图以“同步的形式”编写出异步操作的效果。
以前,C++协程的实现可以通过第三方库来实现的,例如Boost就有两种协程实现(Boost.coroutine2中的协程和Boost.Asio中的协程,见笔者的博文),现在,协程从语言标准层面得以支持,也就是不需要第三方库了。
C++20增加了库<coroutine>,需要协程支持时应包含该文件(【注】gcc10.2编译时需要添加-fcoroutines)。
C++的协程是无栈(stackless)协程,与一个协程关联的有三个对象:
一个promise_type内部对象,保存协程的结果(或发生的异常)
一个handle,从外部可以用来恢复执行或销毁协程
一个可保存coroutine state的内部对象,从堆中分配空间(因而是stackless),包括以下内容:
- 所有的函数参数(值参数和引用参数,这里需要注意的是引用参数,如果协程的生命期比被引用的参数长,则这些引用参数会因不可用而变得很“危险”)
- 当前中断点
- 那些跨越中断点前后的(局部)变量
为表达协程,C++引入了三个关键字:co_return,co_yield和co_await。
co_await:一元操作符。co_await用法为co_await expr,其中expr需要满足一定条件(即awaitable),它要含有以下三个函数:
bool await_ready()
void await_suspend(std::coroutine_handle<> h)
T await_resume()
co_yield :挂起运行,记录状态,返回结果
co_return:结束状态,协程结束
以下类在协程中被经常使用,先需要说明一下它们。
- future:用于获得“将来”状态(异步操作的结果,当前还未完成)。它在C++11时出现,是一个可跨线程的运行结果的包装器,future可以阻塞方式得到异步操作的结果。
- std::suspend_always和std::suspend_never:它们是两个被预定义且满足co_await接口的类,前者的await_ready返回false,后者则返回true。
- coroutine_traits:在编译阶段用来查找和检测满足特定coroutine的promise_type。
- std::coroutine_handle:类似于文件句柄对文件的标识,coroutine_handle用来标识、引用coroutine,例如可以检测、运行、销毁coroutine。coroutine_handle需要一个promise类型作为构造的模板参数,外部可通过promise()函数得到这个特定的promise类。系统库同时定义了一个std::noop_coroutine_handle的类,这个类所代表的coroutine不做任何事情,会挂起。
协程是特殊函数,在一些语言中,例如javascript,python中, 只需在一个函数前添加async关键字就可以定义一个协程函数,但其实它的背后存在大量操作逻辑。虽然现在C++20引入了一些关键字,但目前的实现上,使用协程还不像javascript,python中那么方便,需要为实现这个特殊函数做一些额外的工作。
协程函数的一般构成为:
R coroutine_fuction(args…)
{
co_wait expr
或
co_yield expr
或
co_return expr
//没有return
}
其中R是返回类型。
编译器发现有coroutine函数时,会根据R,args查找struct std::coroutine_traits<R, args…>,如果没有,则产生一个,如果已被定义,就用已被定义的,成功后,检查struct std::coroutine_traits<R, args…>::promise_type是否满足如下描述的coroutine接口:
coroutine接口要求具有public promise_type结构,而promise_type中必须有get_return_object,initial_suspend,final_suspend,unhandled_exception四个函数,另外,如有co_return调用,则promise_type结构中需要return_void/return_value函数;有co_yield调用时,需要yield_value函数;有co_await调用时,需要重载操作符co_await await_transform(expr)或expr,其中await_transform是promise_type结构中一个可选的函数。
如果成功的话,就可以认为该函数是正常的协程函数了。
实际的情况也许比较复杂,因为coroutine_fuction可能是静态函数,非静态函数或右值的非静态函数,对此,cppreference是这样描述的:
- 静态函数时,promise_type的定义为std::coroutine_traits<R, Args...>::promise_type,如task<float> foo(std::string x, bool flag)对应std::coroutine_traits<task<float>, std::string, bool>::promise_type
- 非静态函数时,promise_type的定义为std::coroutine_traits<R, ClassT &, Args...>::promise_type,如task<void> my_class::method1(int x) const对应std::coroutine_traits<task<void>, const my_class&, int>::promise_type
- 右值非静态函数时,promise_type的定义为std::coroutine_traits<R, ClassT&&, Args...>::promise_type,如task<void> my_class::method1(int x) &&对应std::coroutine_traits<task<void>, my_class&&, int>::promise_type
由此,协程的大致运行流程如下:
下面一个示例说明co_return 的用法。
template <typename T>
struct myfuture: std::future<T>
{
myfuture(std::future<T> && a):std::future<T>(std::move(a))
{
std::cout<<"myfuture"<<std::endl;
}
};
template <typename T, typename... Args>
requires(!std::is_void_v<T> && !std::is_reference_v<T>)
struct std::coroutine_traits<myfuture<T>, Args...>
{
struct promise_type:std::promise<T>
{
std::suspend_never initial_suspend() const noexcept { std::cout<<"initial_suspend"<<std::endl; return {}; }
std::suspend_never final_suspend() const noexcept { std::cout<<"final_suspend"<<std::endl; return {}; }
void unhandled_exception() noexcept { this->set_exception(std::current_exception()); }
myfuture<T> get_return_object() noexcept
{
std::cout<<"get_return_object"<<std::endl;
return this->get_future();
}
void return_value(const T &value) noexcept(std::is_nothrow_copy_constructible_v<T>)
{
std::cout<<"return_value"<<std::endl;
this->set_value(value);
}
void return_value(T &&value) noexcept(std::is_nothrow_move_constructible_v<T>)
{
this->set_value(std::move(value));
}
};
};
myfuture<int> co_fun(float a, float b)
{
std::cout << "============co_fun"<<std::endl;
co_return (int)(a * b);
}
int main()
{
auto res1= co_fun(1.1,2.0);
std::cout << "============co_fun return:"<<res1.get()<<std::endl;
}
代码输出如下:
get_return_object
myfuture
initial_suspend
============co_fun
final_suspend
============co_fun return:2
在进入运行co_fun之前,有三行输出,这就是协程流程图中的“准备部分”,由于在initial_suspend中返回的是std::suspend_never,所以程序没有进入suspend。进入co_fun1后,采用co_return 2返回,实际是调用generator:: promise_type::return_value(2),promise_type中的_val保存当前传入的值,退出co_fun1后,采用generator::get_value()得到该值。
以上代码并没有实际的意义,只是为了说明coroutine的流程。
最后
以上就是爱笑心锁为你收集整理的C++20新特性—coroutine协程(一)的全部内容,希望文章能够帮你解决C++20新特性—coroutine协程(一)所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复