赞
踩
官方文档地址
本文主要对c++reference做翻译(不会逐字翻译),同时对其中的概念以及协程运行过程做对应的解释。因为是学习过程中的记录,如有问题,希望大家能够指正,谢谢。也欢迎提问,若看到了一定会第一时间回复,本文会持续更新,保证知识的正确性。
首先介绍什么是协程,协程是一个能够挂起执行流稍后恢复执行的函数。c++20协程是无栈的:它通过主动返回结果给caller来挂起执行流;恢复执行流继续执行所需要的数据将被存储在堆上。协程的一个重要优点就是能够将异步执行转化为同步执行(如各种回调,会导致代码割裂而难以阅读)。
下面介绍几个关键字
co_await,co_yield,co_return
协程不能够使用变长参数,原生的return,或者自动类型推导符auto或者concept,以及const 函数,常量表达式,构造函数,析构函数以及main函数。
协程由下面几个对象组成。
(1) promise object
(2) 函数参数(按值拷贝,注意引用实际上是指针,所以拷贝的是其对象的地址)
(3) 生命周期跨越挂起点的局部变量和临时变量(如 awaiter 对象)
至此上面讲述了协程运行至,co_await expression 的过程,假设co_await挂起的点恢复了,继续向下执行,那下面我们看看继续看看协程怎么结束的。
协程结束有两种方式一种是运行到co_return,另一种方式是抛出异常。
运行到co_return
co_return
co_return expr
执行完函数体。这种方式,如果没有Promise::return_void(),返回结果将是未定义的。
未被捕获的异常
协程也可以通过异常结束,因为它被coroutine handle摧毁了。
coroutine state 通过new 运算符分配在堆内存上。如果promise type类内重载了new运算符,则不会使用全局的new运算符(这里我们是否可以让其分配在我们的内存池上)
如果我们重写了带参数的new 操作运算符,它的参数列表中第一个参数是一个需要分配内存的大小,剩余为协程函数的形参。该分配满足下面两点可能被优化,从而不分配在堆上:
这种情况下,coroutine state将会嵌入到caller的栈帧中(满足编译器的设计的原则,尽可能使用栈内存),如果caller是一个协程,则嵌入到caller的coroutine state 中。
既然分配内存会使用到new,那么自然可能会出现动态内存分配失败的问题。
如果分配失败了,协程会抛出 std::bad_alloc,除非promise type 定义了Promise::get_return_object_on_allocation_failure()。
如果该函数被定义,allocation将会使用不抛出异常的new,当分配失败时,协程就会立刻返回Promise::get_return_object_on_allocation_failure()中获取的对象给caller。
单目运算符co_await将挂起一个协程,然后返回控制权给caller。co_await的操作数是后面的expression,该expression需要定义操作运算符co_await或者实现了Promise::await_transform将类型转换为caller的协程类型。
co_await expression
执行细节
如果await_suspend返回void,将立刻转移控制权给caller/resume,即外层协程。该协程仍然挂起
如果await_suspend返回bool类型:
(1)返回true,说明还未调度,则归还控制权给caller
(2)返回false,说明正在调度中,即立刻resume当前协程
如果await_suspend返回一个协程句柄coroutine handle,则该handle将用来resume该协程(如handle.resume)。这种方法将最终导致链式恢复。
如果await_suspend中抛出了异常,且在内部被捕获。则在协程恢复后(resume),异常将立刻再次抛出。
+最后,无论协程是否挂起,都会调用awaiter.await_resume(),其结果将是整个co_await expression 表达式的返回值。
如果协程在 co_await expression中被挂起,则恢复点将在该awaiter.await_resume()之前。
【注意】由于在进入awaiter.await_suspend()前,协程已经完全挂起了,所以跨线程传递coroutine handle是线程安全的,无需其他的同步。比如,我们可以将其传入一个回调函数中,并将该回调交给线程池处理后续逻辑处理。注意传递是线程安全的,而执行可能不是。假设在主线程A中我们呢调用co_await expression 创建了一个awaiter,然后在awaiter.await_suspend(handle)中,将handle交给了线程池,则后续执行resume时,需要小心再A线程中resume过了。所以我们的原则是:对待*this为已经被销毁了,再分配给其他线程后不要再access该协程。
【注意】协程对象是coroutine state 的一部分。作为一个临时变量,它的生命周期跨越一整个挂起点,它将会在co_await expression 完成前销毁。
至此reference翻译完了。接下来我们结合代码来看吧。
#define __cpp_lib_coroutine #include <iostream> #include <coroutine> #include <future> #include <chrono> using namespace std::chrono_literals; // 这个是用来做函数协程返回的 struct Result { struct promise_type { std::suspend_never initial_suspend() { std::cout << "Result::initial_suspend" << std::endl; return {}; } std::suspend_never final_suspend() noexcept { std::cout << "Result::final_suspend" << std::endl; return {}; } Result get_return_object() { std::cout << "Result::get_return_object" << std::endl; return {}; } void return_void() { } void unhandled_exception() { } }; }; struct Result2 { struct promise_type { std::suspend_never initial_suspend() { std::cout << "Result2::initial_suspend" << std::endl; return {}; } std::suspend_never final_suspend() noexcept { std::cout << "Result2::final_suspend" << std::endl; return {}; } Result2 get_return_object() { std::cout << "Result2::get_return_object" << std::endl; return {}; } void return_void() { } void unhandled_exception() { } }; }; Result2 Coroutine2(int * value) { std::cout << "Coroutine2" << std::endl; std::cout << *value << std::endl; co_return; } Result Coroutine(int * value) { std::cout << "Coroutine" << std::endl; std::cout << *value << std::endl; Coroutine2(value); std::cout << "Coroutine end" << std::endl; co_return; }; int main() { int num = 99; Coroutine(&num); std::cout << "end program" << std::endl; return 0; }
输出:
Result::get_return_object
Result::initial_suspend
Coroutine
99
Result2::get_return_object
Result2::initial_suspend
Coroutine2
99
Result2::final_suspend
Coroutine end
Result::final_suspend
end program
分析:
定义了两种协程,且initial_suspend,final_suspend返回均为std::suspend_never。这种其实就和普通函数没有区别,且在这种情况下coroutine state将嵌入到栈帧中去,而不是分配在堆空间上,所以不需要用指针去访问对象,效率更高
接下来我们将对上述代码进行修改
输出:
Result::get_return_object
Result::initial_suspend
end program
分析:
这里我们发现协程中所有都没有打印,这里的原因是:普通函数main调用了couroutine,协程initial_suspend后返回suspend_always,所以创建了result这个协程对象并挂起,但是普通函数继续往下执行直至进程生命周期结束,也没有resume。
输出:
Result::get_return_object
Result::initial_suspend
Coroutine
99
Result2::get_return_object
Result2::initial_suspend
Coroutine end
Result::final_suspend
end program
分析:
分析上述结果我们可以知道当协程coroutine运行到Coroutine2(value) 时,创建result协程对象并挂起,所以不向之前普通函数调用协程那样直接运行后面的语句。
suspend_never
标准库中的awaiter object,结构如下:
我们可以用自己的类来替代,继续看下这几个函数的调用规则和机制。
struct suspend { bool await_ready() const noexcept { std::cout << "await_ready" << std::endl; return false; } void await_suspend(std::coroutine_handle<>) const noexcept { std::cout << "await_suspend" << std::endl; } void await_resume() const noexcept { std::cout << "await_resume" << std::endl; } }; struct Result { struct promise_type { suspend initial_suspend() { std::cout << "Result::initial_suspend" << std::endl; return {}; } suspend final_suspend() noexcept { std::cout << "Result::final_suspend" << std::endl; return {}; } Result get_return_object() { std::cout << "Result::get_return_object" << std::endl; return {}; } void return_void() { } void unhandled_exception() { } }; }; Result Coroutine(int * value) { std::cout << "Coroutine" << std::endl; std::cout << *value << std::endl; std::cout << "Coroutine end" << std::endl; co_return; }; int main() { int num = 99; Coroutine(&num); std::cout << "end program" << std::endl; return 0; }
输出:
Result::get_return_object
Result::initial_suspend
await_ready
await_suspend
end program
分析
这次我们关注await_*。当await_ready返回false,则说明还没准备好resume,接着调用await_suspend,如果返回void,则直接将控制权归还给调用函数,然后又因为数据没有准备好,所以理所应当不执行resume。
输出
分析
当返回值为true时,则直接进行resume,将不会去根据await_suspend判断协程是否可立即调度。
经过上面,我们可以确定,当await_suspend返回true时,就会忽略await_suspend直接去调await_resume了。下面我们继续看下await_suspend返回false的两种情况。
输出
Result::get_return_object
Result::initial_suspend
await_ready
await_suspend
end program
分析
可见,当未准备好,且不可立即调度(await_suspend 返回false 可以理解为,该awaiter 外的线程对象result这在调度中,所以可以立即执行await_resume)
输出
分析
协程对象result,能数据没准备好,但是正在调度中,所以可以立即执行await_resume 来恢复执行。
再分析下本次结果
分析
如上就是一个协程挂起和恢复的全部过程:
- promise_type对象调用get_return_object创建协程对象result
- 调用协程对象result的initial_suspend
由于awaiter对象suspend此时未false,true,可以直接resume 协程对象result
所以执行协程体coroutine
执行完毕后调用final_suspend,由于该函数也会返回suspend,所以还会调用一边awaiter的流程
执行完resume,result对象就释放了,suspend作为result的局部对象,也会释放掉。suspend可以反映协程对象的状态。
至此我们还没有使用过co_await,既然不用co_await协程依旧可以挂起,那为什么还要使用co_await呢?下面我们对比下使用co_await的区别
取消注释更改代码结构如下:
#define __cpp_lib_coroutine #include <iostream> #include <coroutine> #include <future> #include <chrono> using namespace std::chrono_literals; struct suspend { bool await_ready() const noexcept { // std::cout << "await_ready" << std::endl; return true; } bool await_suspend(std::coroutine_handle<>) const noexcept { // std::cout << "await_suspend" << std::endl; return true; } void await_resume() const noexcept { // std::cout << "await_resume" << std::endl; } }; struct Result { struct promise_type { suspend initial_suspend() { std::cout << "Result::initial_suspend" << std::endl; return {}; } suspend final_suspend() noexcept { std::cout << "Result::final_suspend" << std::endl; return {}; } Result get_return_object() { std::cout << "Result::get_return_object" << std::endl; return {}; } // void operator co_await(){ // std::cout << "co_await" << std::endl; // } void return_void() { } void unhandled_exception() { } }; }; suspend Coroutine2(int * value) { std::cout << "Coroutine2" << std::endl; // std::cout << *value << std::endl; // std::cout << "Coroutine2 end" << std::endl; return {}; }; Result Coroutine(int * value) { std::cout << "Coroutine" << std::endl; // std::cout << *value << std::endl; Coroutine2(value); std::cout << "Coroutine end" << std::endl; co_return; }; int main() { int num = 99; Coroutine(&num); std::cout << "end program" << std::endl; return 0; }
修改下面代码
经过测试我们发现:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。