引言
由于应用程序的主线程是不应该被卡住的,所以对于一些耗时的操作(如IO操作),我们都会采取一些措施去防止主线程卡住。通常由以下措施防止这个问题。
1、开线程同步执行
2、使用异步IO模型,定时获取IO完成状态(如完成端口、Poll等)
3、设置回调(如Http)
其流程图如下:
c++20引入了协程这个框架,但协程比普通函数只多了一个挂起操作,所以如果在主线程(主协程)开启协程执行耗时操作,这个时候主线程还是会被卡住,所以我们应该将耗时的任务还是交给后台(线程)执行,并暂停这个协程,让其恢复主协程,在主协程检查是否恢复暂停的协程、创建新的协程以及执行协程等操作,其流程图如下:
这里省略了后台执行的流程以及完成之后通知唤醒的流程 ,只体现了协程的主流程。通过对比我们不难发现,跟非协程版本的相比,我们的编程流程变成了一个线性逻辑,并且减少了多线程同步的操作。至于后台执行逻辑都是相同的。所以我们很简单的就能将一个耗时任务改装成一个协程任务。但要解决以下问题
- 如何暂停
- 何时恢复
- 如何恢复
关于如何暂停一个协程,可以参考协程关键类接口说明。这里我们主要讲如何暂停当前协程。说到这里,我们就要提到Awaiter关键类了,因为co_await expr中expr会被转成awaiter对象。
如何暂停协程
void返回值版本
void await_suspend(std::coroutine_handle<> co_handle)
当此接口被调用时,该协程会被暂停执行
bool返回值版本
bool await_suspend(std::coroutine_handle<> co_handle)
当返回false时,跟void返回值版本行为一致
当返回true时,则立即恢复当前协程并继续执行, 防止栈溢出
std::coroutine_handle<>返回值版本
std::coroutine_handle<> await_suspend(std::coroutine_handle<> co_handle)
暂停当前协程,转移执行到返回的协程执行(解决递归调用的方法,它允许一个协程在挂起后将执行权转交给另一个协程,而不会消耗额外的栈空间。)
代码示例
cppcoro::task<int> Fool() {
int result = co_await Awaiter();;
co_return result;
}
cppcoro::task<int> Bar() {
auto result = co_await Fool();
co_return result;
}
可以看到,我这里直接co_awaiter一个awaiter这样co_awaiter回去评估其await_suspend,如何返回值为void或者false,则Fool协程被暂停,但是在执行await_suspend的里面,我们可以添加一个将某个耗时任务放入后台的操作,这样协程虽然暂停了,但是任务还是可以执行。
何时恢复
通过上诉操作之后,执行耗时任务的协程被暂停了,但这个耗时任务被丢到了后台运行,那其恢复的时机自然而然则是等到这个被丢到后台的耗时任务完成。当然,如果不关心返回值的话是可以不用去暂停这个协程的。
如何恢复
现在到了最关键的一步,我们知道想要恢复协程,必须用其协程句柄,所以我们要通过这个任务来获取其暂停的协程句柄,至于这两个的关系如何映射,我们这里就不展开讨论了。但是为了线程安全,我们还是应该回到主线程去调用句柄恢复执行。
总结
通过上面的描述,我们知道,这里涉及到几个队列,协程任务完成队列(待恢复队列)和任务队列(后台执行任务),有了这些,再加上在特定的位置暂停协程,我们就能自己完成对协程的执行流做封装,并且将一些网络库、IO操作变成协程调用。减少线程同步以及回调等逻辑,让程序线性执行。