网站建站咨询,wordpress5.0难用,抽奖网站建设,如何通过ftp上传网站一、协程
在前面分析过协程是什么#xff0c;也对c20中的协程的应用进行了举例说明。所以这次重点分析一下c20中的整体构成及应用的方式。等明白了协程是如何动作的#xff0c;各种情况下如下何处理相关的事件#xff0c;那么在以后写协程时就不会死搬硬套了。
二、整体说…一、协程
在前面分析过协程是什么也对c20中的协程的应用进行了举例说明。所以这次重点分析一下c20中的整体构成及应用的方式。等明白了协程是如何动作的各种情况下如下何处理相关的事件那么在以后写协程时就不会死搬硬套了。
二、整体说明
在C20中的协程中首先要明白几个主要的类型和关键字。在协程中可以划分为三种类型以及一种状态这三种类型或者说表现出来的数据结构是 1、promisepromise_type 这个在C11中就有此处和其意义相似协程内部通过此对象来获取结果及异常。协程中要求返回对象必须是promis_type,这是一个接口类型。它一般要实现
get_return_object 获取协程的返回对象
initial_suspend协程初始化的时挂起
final_suspend协程结束时挂起的操作initial_suspend和final_suspend 有点象钩子函数也有点像JAVA的Spring中的拦截器前者在执行协程函数前运行后者在协程返回前执行。这里需要注意的是如果final_suspend 运行了协程挂起的动作比如使用了std::suspend_always{}就需要记得在协程结束后手动destroy来销毁否则的话协程将自动销毁。这个和线程的detach和join有点类似。 另外它还有其它一些方法
void return_void(): 如果没有实现co_return或单纯调用co_return以及co_return expr中expr返回值为void,必须有否则报错
void return_value()co_return expr中expr返回值为非void
void yield_value()如果程序中调用了co_yield则必须有
unhandled_exception(): 异常控制同时promise_type还会管理协程的相关栈空间即一些临时变量、参数、返回值及一些寄存器的上下文等。
2、awaitable(awaiter) awaitable和awaiter是比较让初学者有些感到迷惑的有人可能觉得为什么要有两个类似的对象。其实这个非常好理解前者是用来处理co_wait的expr的对象后者是具体的处理对象或者等待器。而awaitable可以理解为居中的一层抽象虽然co_wait awaiter但直接使用awaiter便不再依赖于抽象而是依赖于具体实现。所以便把awaiter抽象出来具体由awaitable来实现。所以expr类型需要是一个awaitable类型。而awaitable可以转换成awaiter。 它其中需要实现的接口包括
await_ready()是否挂起协程。返回 trueco_await不挂起函数否则挂起一般异步都是false并调用await_suspend
await_resume()co_await expt的返回值一般为空如果有返回值则 auto ret co_await expt
await_suspend(handle)协程挂起时的行为可能通过handle得到当前协程的状态以此来处理挂起时的动作在c20的协程说明中有详细的说明哪几种情况可以转换成awaitable。如果没有什么复杂的动作可以使用在标准库里实现的两个此类型的结构体
struct suspend_never {_NODISCARD constexpr bool await_ready() const noexcept {return true;}constexpr void await_suspend(coroutine_handle) const noexcept {}constexpr void await_resume() const noexcept {}
};struct suspend_always {_NODISCARD constexpr bool await_ready() const noexcept {return false;}constexpr void await_suspend(coroutine_handle) const noexcept {}constexpr void await_resume() const noexcept {}
};代码很简单注意noexcept,必须有。 这里重点需要说明一下await_suspend其实异步处理最主要的就是在此处它可以通过传入的外部包裹的协程handle来管理协程动作。所以在网上或者资料上的一些例程在此处直接调用resume其实是没有展现出异步的作用。 另外在await_suspend中的参数如果有操作promise_type则可以写成std::coroutine_handle这就是默认void或自动推导就可以了。
3、coroutine_handle 这个就是刚刚提到的std::coroutine_handle,它一般常用的有两个方法
handle.resume()协程恢复执行
handle.destroy()销毁协程在前文提到的final_suspend挂起时当然还donefrom_address等接口大家可以参考相关资料文档。 需要注意的是这个句柄类似于浅拷贝析构函数不处理相关的协程内部状态的内存需要操作destroy函数但大多数情况下都是自动销毁只有在前面提到的那种情况下需要手动调用destroy函数。同样销毁掉句柄后被复制的句柄成为了类似于悬垂指针的存在。
而一种状态是协程状态coroutine state它主要有包含以下几点 1、the promise object 2、the parameters (all copied by value) 3、some representation of the current suspension point, so that a resume knows where to continue, and a destroy knows what local variables were in scope这个有点类似于线程上下文的意思 4、local variables and temporaries whose lifetime spans the current suspension point. 协程的状态有点类似于线程的上下文这个需要大家自己去体会。
在协程中主要有三个关键字co_wait,co_yield和co_return
1、co_wait 它是一个一元运算符用来处理协程的暂停并将控制权返回给调用者直到重新恢复协程其操作的是一个表达式即co_wati expr即awaiter 。和上面的awaitable对应。 2、co_yield 这个相当于暂停协程并返回一个值而这个值可以被重新利用即前面提到的yield_value函数。 3、co_return 这个好理解它直接就返回值并结束协程。
这样把三种类型的概念和协程的状态以及三个关键字的关系搞清楚是不是协程的用法已经呼之欲出小的细节可以在实际应用中踩踩坑并多看文档来处理。
三、协程的执行流程
看完上面的分析下面再把它们的关系用流程串边一下即协程启动后它们之间如何配合工作的 1、首先分配内存并初始化协程状态 2、相关参数复制到协程 3、构造promise对象 4、通过调用 promise.get_return_object()并保存结果供协程首次挂起返回值给调用者 5、执行co_await promise.initial_suspend()如果无复杂需求可使用STL中定义的 std::suspend_always 始终挂起和std::suspend_never永不挂起 6.1 协程函数执行至 co_return expr语句 若 expr 为 void 则执行 promise.return_void()否则执行 promise.return_value(expr) 按照创建顺序的倒序销毁局部变量和临时变量 执行 co_await promise.final_suspend()
6.2 当协程执行到 co_yield expr语句 调用co_await promise.yield_value(expr)
6.3 当协程执行到 co_await expr语句 通过 expr 获得 awaiter 对象。 执行 awaiter.await_ready()若为 true 则直接返回awaiter.await_resume()否则将协程挂起并保存状态执行 awaiter.await_suspend()若其返回值为 void 或者 true 则成功挂起将控制权返还给调用者直到 handle.resume() 执行后该协程才会恢复执行将 awaiter.await_resume() 作为表达式的返回值
6.4 当协程因为某个未捕获的异常导致终止 捕获异常并调用 promise.unhandled_exception() 调用 co_await promise.final_suspend() 并 co_await 它的结果例如恢复某个继续或发布某个结果。此时开始恢复协程是未定义行为。
6.5 当协程状态销毁时通过协程句柄主动销毁 / co_return 返回 / 未捕获异常 调用promise 对象的析构函数 调用传入参数的析构函数 释放协程状态占用内存 转移执行回调用方/恢复方
以上这些在官方文档中有更详细的说明大家可以在实际开发中去查阅相关信息。 是不是有点复杂是有点复杂啊。这个样子还是不能让普通程序员用得自由也就是没有解决简单的问题所以协程还需要继续努力网上有各种基于此的协程库。
四、例程
这里的例程重点是对上面的分析的一种代码的说明请仔细看代码并和上面的分析说明对照会有更深的理解先看一个例子
#include coroutine
#include iostream
#include stdexcept
#include threadauto switch_to_new_thread(std::jthread out)
{struct awaitable{std::jthread* p_out;bool await_ready() { return false; }void await_suspend(std::coroutine_handle h){std::jthread out *p_out;if (out.joinable())throw std::runtime_error(jthread 输出参数非空);out std::jthread([h] { h.resume(); });// 潜在的未定义行为访问潜在被销毁的 *this// std::cout 新线程 ID p_out-get_id() \n;std::cout 新线程 ID out.get_id() \n; // 这样没问题}void await_resume() {}};return awaitable{out};
}struct task
{struct promise_type{task get_return_object() { return {}; }std::suspend_never initial_suspend() { return {}; }std::suspend_never final_suspend() noexcept { return {}; }void return_void() {} //和上文中的说明对照void unhandled_exception() {}};
};task resuming_on_new_thread(std::jthread out)
{std::cout 协程开始线程 ID std::this_thread::get_id() \n;co_await switch_to_new_thread(out);// 等待器在此销毁std::cout 协程恢复线程 ID std::this_thread::get_id() \n;
}int main()
{std::jthread out;resuming_on_new_thread(out);
}
可能的输出
协程开始线程 ID139972277602112
新线程 ID139972267284224
协程恢复线程 ID139972267284224怎么样协程进行了线程的自动调度方便很多吧。但是这种在不同线程间调度时需要注意不能在另外一个线程内恢复协程否则容易出现数据处理的问题这个就看应用的场景了。用不好就直接挂了。这个例子中也有风险提示大家看一下就明白了。 再看一个简单的例子
#include coroutine
#include iostreamstruct MyCoroutine {struct MyPromise {MyCoroutine get_return_object() {return std::coroutine_handleMyPromise::from_promise(*this);}std::suspend_never initial_suspend() { return {}; }// 使用suspend_always需要手动 destroy,noexcept必须auto final_suspend() noexcept{ return std::suspend_always{}; }void unhandled_exception() {}void return_void() {} //返回空必须有};using promise_type MyPromise;//尖括号中MyPromise类型可自动推导MyCoroutine(std::coroutine_handle h) : handle(h) {}std::coroutine_handleMyPromise handle;};MyCoroutine first() {std::cout this is my first \n ;co_await std::suspend_always{};std::cout coroutine!\n;
}int main() {MyCoroutine my first();my.handle.resume();my.handle.destroy();//手动释放return 0;
}看到上面的第一个例子中的风险那么如何安全的调度协程实现await_suspend的异步操作情况两种方法一种是在主线程中写一个任务处理函数不断的去探查协程挂起后的操作并把await_suspend的恢复放到主线程中去。第二种方法就是使用和线程池类似的方式将任务注册到队列通过消息来驱动协程工作并最终将任务结果返回。 github上有阿里开源的协程库应用起来会更简单https://github.com/alibaba/async_simple。 源码面前了无秘密。
五、总结
协程最大特点是它可以跨越线程来进行操作而在线程中一般数据处理要么在线程中独自控制要么需要加锁。所以协程应用起来更灵活这也是为什么协程能更好的发挥线程的作用并同时呈现更好异步操作的原因。这个在GO的协程测试中已经有验证协程可以开出几百万个但线程一般到几百个就达到瓶颈了。 协程的开发会在大多数场景下替代线程的开发只有一个最大原因简单功能强大倒是其次。希望c中的协程在后面会变得更简单好用。