当前位置: 首页 > 科技观察

C++20协程Coroutine

时间:2023-03-12 19:31:23 科技观察

C++20协程C++20有个新东西,协程。这个东西可能是C++未来的重要组成部分,也会让C++成为服务器编程的利器。对于C++20协程,对协程最简单的理解就是可以重入的特殊函数。执行这个函数的过程可以被挂起(通过co_await,或者co_yield),然后在外部恢复(通过coroutine_handle)。我测试的代码都是在VisualStudio2022上跑的,据说GCC10.0也已经支持了。协程是特殊函数首先再次强调,C++20的协程是特殊函数。只不过这个函数具有挂起和恢复的能力,可以挂起(调用代码挂起后继续向后执行),然后它的执行可以继续恢复。如下图所示:如图所示,协程不会执行一次,可以重复挂起。暂停后可恢复至暂停点继续运行。C++20协程的特点下面我们就来看看C++20协程的一些特点和用途。首先,C++20协程是stackless协程。同时C++20协程是一个非对称协程,区别于Linux传统的ContextSwitch。更像是Windows的纤程。也类似于C#的协程,毕竟是微软的提案。传统的上下文切换有一堆协程。你可以认为上下文协程都是在栈上运行的,上下文协程的切换就是栈的切换。同时因为它有一个栈协程。交换是对称的,是堆叠交换。您可以从主线程切换到另一个Context协程堆栈,从一个Context协程切换到主线程,或者在Context协程之间切换。Context协程的状态保存在栈上。C++20协程可以用来做什么?与大多数协程类似,它用于异步编程。看图一可以理解,每次协程挂起,都可以看成是协程进入了等待状态。比如请求网络,需要HTTP获取一个文件,然后分析这个文件。然后就可以用一个协程来包装整个过程。发起HTTP请求后,挂起协程(处理其他事情),等待响应或超时,再恢复协程。但缺点是目前的C++20协程只是一个开始。说实话,现在的协程只提供了基础框架,写起来不太舒服。C++目前在IO方面还不够完善,尤其是网络IO。要用好C++20的协程,需要大量的异步IO库。如果C++20的协程外设更完备,或许C++在服务器编程上可以再次面临Go之类语言的威胁。C++协程有三个关键字。C++协程(coroutinefunction)可以使用co_await,co_yield。两个关键字挂起协程,co_return,关键字return。co_awaitco_await调用一个awaiter对象(可以认为是一个接口),根据其内部定义,以及挂起和恢复时的行为决定其操作是挂起还是继续。它的表现形式是cw_ret=co_awaitawaiter;cw_ret记录了调用的返回值,也就是awaiter的await_resume接口的返回值。co_await比较复杂,后面的章节会详细讲解。co_yield暂停协程。它的形式是co_yieldcy_ret;cy_ret将存储在promise对象中(通过yield_value函数)。可以通过协程外的promise获取。co_return协程返回。它的形式是co_returncr_ret;cr_ret将存储在promise对象中(通过return_value函数)。可以通过协程外的promise获取。需要注意的是cr_ret并不是协程的返回值。这个是不同的。C++协程的重要概念C++编译器是如何识别协程函数的?是函数的返回值。C++协程函数的返回类型是必需的。返回类型为result,result中必须有一个子类型promise对象(promise),表现为Result::promise_type。承诺对象(promise)是一个接口,实现了get_return_object等接口。并且通过静态函数std::coroutine_handle::from_promise(promise&p),我们可以得到协程句柄(coroutinehandle)。协程的运行状态、协程函数的形参、内部变量、临时变量、挂起的点都保存在协程状态(coroutinestate)中。好了,从上面的描述中,我们可以看出协程的几个重要概念。协程状态(coroutinestate),记录了协程的状态,是分配在堆上的内部对象:promise对象形参(协程函数的参数),临时变量promise对象(promise)在该点协程暂停的地方,从协程内部操作。协程通过该对象提交它们的结果或异常。协程句柄,一个协程的唯一标识。用于恢复协程执行或销毁协程框架。Awaiter,通过co_await关键字调用的对象。协程状态(coroutinestate)是协程启动时,新的空间存放协程状态,协程状态记录了协程函数的参数,协程的运行状态,变量。挂断点。注意协程状态不是协程函数的返回值RET。虽然我们设计的RET一般都有promise和coroutinehandle,但是大家一般都是用RET来操作coroutine的recovery,获取返回值。但是协程状态理论上应该也包含协程运行参数、断点等信息。协程状态应该是协程句柄对应的一段数据,由系统管理。承诺对象(promise)承诺对象的表达形式必须是result::promise_type,result是协程函数的返回值。Promise对象是几个接口的实现,用来辅助协程,构造协程函数的返回值;提交并传递co_yield和co_return的返回值。指定协程启动阶段是否立即挂起;以及如何处理协程内部的异常。它的接口包括:autoget_return_object():用于生成协程函数的返回对象。autoinitial_suspend():用于明确协程函数初始化后的执行行为,返回值是awaiter,返回值是用co_wait调用的。std::suspend_always的返回值表示协程启动后立即挂起(不执行协程函数第一行代码),std::suspend_never的返回值表示协程不立即挂起开始后。(当然,既然是返回等待体,这里可以选择做什么样的等待操作)voidreturn_value(Tv):调用co_returnv后会调用该函数,可以保存co_return的结果autoyield_value(Tv):调用co_yield后会调用这个函数,可以保存co_yield的结果,其返回值为std::suspend_always,表示协程会被挂起。如果返回std::suspend_never,表示不会被挂起。autofinal_suspend()noexcept:协程退出时调用的接口,返回std::suspend_never,自动销毁协程状态对象。如果final_suspend返回std::suspend_always,用户需要调用handle.destroy()销毁它。但值得注意的是,返回std::suspend_always并不会挂起协程。前面我们提到,创建协程时,协程状态会是new的。您可以通过在promise_type中重载operatornew和operatordelete来使用自己的内存分配接口。(请参考RevisitingC++20Coroutine)协程句柄(coroutinehandle)是一个协程标记,用于操作协程回收和销毁句柄。协程句柄的表达形式为std::coroutine_handle,其模板参数为promise对象(promise)类型。handle有几个重要的功能:resume()函数可以恢复协程。done()函数可以判断协程是否已经完成。返回false表示协程还没有完成,还在pending。协程句柄和承诺对象可以相互转换。std::coroutine_handle::from_promise:这是一个静态函数,可以从promise对象(promise)中获取对应的handle。std::coroutine_handle::promise()函数可以从协程句柄coroutinehandle中得到对应的承诺对象(promise)等待者(awaiter)。co_wait关键字会调用一个等待者对象(awaiter)。该对象内部还有3个接口。根据接口co_wait决定做什么。boolawait_ready():等待体是否就绪,返回false,表示协程没有就绪,立即调用await_suspend。返回真,表示准备就绪。autoawait_suspend(std::coroutine_handle<>handle)如果要挂起,调用接口。其中handle参数是调用等待体的协程,其返回值有三种可能:void返回truebool,返回true立即挂起,返回false不挂起。返回一个协程句柄,立即恢复对应句柄的操作。autoawait_resume():协程挂起后恢复时调用的接口。返回值用作co_wait操作的返回值。服务员值得一章更详细,所以让我们就此打住并了解它有2个专业。std::suspend_never类,一种不挂起的专用等待体类型。std::suspend_always类,一种专门用于挂起的等待体类型。之前的很多接口都用到了这两个特化类,也可以理解为协程内部其实很多地方都用到了co_wait关键字。例如,“七进七出”协程。好吧。我们介绍的所有概念都基本完成了。让我们从一些代码开始。不然真的很郁闷。这个例子主要展示了协程函数和主线程之间的切换。协程反复中断然后在主函数内恢复执行。直到最后一次co_return。这个例子虽然简单,但是如果你对异步编程有一点了解,你就可以理解如何使用C++20来完成一段异步编程。获取源码地址请点击以下例子:coro_retcoroutine_7in7out()为协程函数。coro_retc_r是协程的返回值。后续所有的交互都是通过c_r和coroutines进行的。coro_ret::promise_type是承诺对象std::coroutine_handle是句柄。#include#include#include#include//!coro_ret协程函数的返回值,promise_type内部定义,promise对象模板structcoro_ret{struct承诺类型;使用handle_type=std::coroutine_handle;//!协程句柄handle_typecoro_handle_;coro_ret(handle_typeh):coro_handle_(h){}coro_ret(constcoro_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=(constcoro_ret&)=delete;coro_ret&operator=(coro_ret&&s){coro_handle_=s.coro_handle_;s.coro_handle_=nullptr;返回*这个;}//!恢复协程,returnboolmove_next(){coro_handle_.resume();返回coro_handle_.done();}//!通过promise获取数据,返回值Tget(){returncoro_handle_.promise().return_data_;}//!promise_type为promise对象,用于协程内外通信。structpromise_type{promise_type()=默认值;~promise_type()=默认值;//!生成协程返回值autoget_return_object(){returncoro_ret{handle_type::from_promise(*this)};}//!请注意,此函数返回awaiter//!如果返回std::suspend_never{},则不会挂起,//!返回std::suspend_always{}以暂停//!当然你也可以returnotherawaiterautoinitial_suspend(){//returnstd::suspend_never{};返回std::suspend_always{};}//!co_return之后这个函数会被调用voidreturn_value(Tv){return_data_=v;返回;}//!autoyield_value(Tv){std::cout<<“调用了yield_value。”<<标准::结束;return_data_=v;返回std::suspend_always{};}//!协程退出后调用的接口//!如果final_suspend返回std::suspend_always,则用户需要调用//!handle.destroy()销毁,但注意协程在调用final_suspend时已经结束//!返回std::suspend_always不会暂停协程进程(在VSC++2022上测量)autofinal_suspend()noexcept{std::cout<<"final_suspendinvoked."<<标准::结束;返回std::suspend_always{};}//voidunhandled_exception(){std::exit(1);}//返回值Treturn_data_;};};//这是一个协程函数coro_retcoroutine_7in7out(){//进入协程查看initial_suspend,returnstd::suspend_always{};将有一次暂停std::cout<<"Coroutineco_awaitstd::suspend_never"<