前言
前面两节我们已经能够实现一个可用的协程框架了。但我们一定还想更深入的了解协程,于是我们就想尝试下能不能co_await一个协程。下面会涉及到部分模板编程的知识,主要包括(模板偏特化,模板参数列表传值,模板函数类型推断)可以提前了解下,方便后续理解。
在开始之前我们不妨思考下面一些问题:
- 异步函数 和 协程 在co_await时的共性是什么?
两者的操作数都必须是awaiter,这意味着co_await协程,必须能够将协程转换为一个awaiter。- 协程生命周期什么时候结束?协程执行到co_return时,或者执行coroutine_handle.destory()时,以及出现未捕获的异常时会销毁协程,释放协程对象promise
- 协程从开始到结束,会产生几个awaiter,会co_awiter几次?
2~3次,initial_suspend和final_suspend会产生两个awaiter,同时编译器帮我们进行了co_await, 还有一次是人为的co_await,即我们co_await一个协程。- initial_suspend和final_suspend 这两个功能的作用是什么?
为什么要initial_suspend和final_suspend ,或者说为什么这里可以自由返回可挂起的等待体,为什么提供这个机制?initial_suspend是协程创建后,编译器帮我们co_await,这将允许我们即使不使用co_await,协程函数运行能参与到不同于普通函数的调度中,这直接决定了协程行为上和普通函数的相似程度;final_suspend功能也类似,我们已经知道该函数是在协程执行结束时操作系统使用co_await调用的,如果final_suspend返回的是不挂起操作awaiter,那么协程在执行完后会自动析构promise对象释放资源,而返回挂起awaiter,提供了将协程对象销毁交给用户的协程调度器的可能性。这里还有一个知识点,是对第二篇final_suspend的补充,使用协程还可以实现序列发生器,序列发生器中的协程永远不会调用co_return,所以永远不会结束,当final_suspend不挂起时,编译器也无法分辨出一个协程的生命周期,而这里选择挂起,我们可以明确告诉编译器该协程会结束,有助于编译器帮我们优化。
协程代码实现
我的目标是让c++像在python中一样使用协程。
我的协程实现思路如下:我希望协程表现出的行为尽可能和普通函数一样,所以我不在initial_suspend时挂起协程给协程调度器调度(我直接在该返回的awaiter::await_ready返回true,给编译器提供优化协程为inline的机会);协程应该是和python一样是单线程,所以不会在用户co_await时,交给其他线程处理(这部分功能由上一节异步函数补齐);我希望编译器尽可能能帮我优化代码而且更符合规范,所以我在协程结束时挂起协程,交给调度器去调度销毁。
代码
#include <coroutine>
#include <future>
#include <chrono>
#include <iostream>
#include <queue>
#include <thread>
#include <mutex>
#include <memory>
#include <vector>
struct async_task_base
{
virtual void completed() = 0;
virtual void resume() = 0;
};
std::mutex m;
std::vector<std::shared_ptr<async_task_base>> g_event_loop_queue;
std::vector<std::shared_ptr<async_task_base>> g_resume_queue; //多线程异步任务完成后后,待主线程恢复的线程
std::vector<std::shared_ptr<async_task_base>> g_work_queue; //执行耗时操作线程队列
enum class EnumAwaiterType:uint32_t{
EnumInitial = 1, //协程initial
EnumSchduling = 2,// 用户co_await
EnumFinal = 3//销毁
};
template <typename ReturnType>
struct CoroutineTask;
template <typename CoTask, EnumAwaiterType AwaiterType >
struct CommonAwaiter ;
template <typename CoTask, EnumAwaiterType AwaiterType>
struct coroutine_task: public async_task_base{
coroutine_task(CommonAwaiter<CoTask, AwaiterType> &awaiter)
:owner_(awaiter)
{
}
void completed() override{
}
void resume() override{
if(owner_.h_.done()){
owner_.h_.destroy();
}else{
owner_.h_.resume();
}
}
CommonAwaiter<CoTask,AwaiterType> &owner_ ;
};
template <typename CoTask, EnumAwaiterType AwaiterType = EnumAwaiterType::EnumSchduling>
struct CommonAwaiter
{
using return_type = typename CoTask::return_type;
using promise_type = typename CoTask::promise_type;
CommonAwaiter(promise_type* promise):promise_(promise){
}
// 当时initial_suspend返回的awaiter时,挂起,直接resume
bool await_ready() const noexcept {
return false;
}
//也可以直接恢复
// std::coroutine_handle<> await_suspend(std::coroutine_handle<> h) {
// return h;
// }
void await_suspend(std::coroutine_handle<> h) {
// std::cout <<"await_suspend()" << std::endl;
h_ = h;
g_event_loop_queue.emplace_back(std::shared_ptr<async_task_base>( new coroutine_task<CoTask, AwaiterType>(*this)) );
}
return_type await_resume() const noexcept {
return promise_->get_value();
}
~CommonAwaiter(){
}
bool resume_ready_= false;
promise_type* promise_ = nullptr;
std::coroutine_handle<> h_ = nullptr;
};
template <typename CoTask>
struct CommonAwaiter<CoTask, EnumAwaiterType::EnumInitial>
{
CommonAwaiter(){
}
// 当时initial_suspend返回的awaiter时,挂起,跳过await_suspend,直接resume,跳过
bool await_ready() const noexcept {
return true;
}
void await_suspend(std::coroutine_handle<>) {
}
void await_resume() const noexcept {
}
~CommonAwaiter(){
}
};
// 必须为noexcept,因为这个时候协程已经运行结束,不该有异常产生
template <typename CoTask>
struct CommonAwaiter <CoTask, EnumAwaiterType::EnumFinal>
{
CommonAwaiter(){
}
// 这里不选择true让编译器帮我们自动释放,如果为true编译器不知道什么时候协程结束,无法帮助我们优化
bool await_ready() noexcept {
return false;
}
void await_suspend(std::coroutine_handle<> h) noexcept{
h_ = h;
g_event_loop_queue.emplace_back(std::shared_ptr<async_task_base>( new coroutine_task<CoTask, EnumAwaiterType::EnumFinal>(*this)));
}
// 无需返回
void await_resume() noexcept{
}
std::coroutine_handle<> h_ = nullptr;
};
template<typename CoTask>
struct Promise
{
using return_type = typename CoTask::return_type ;
~Promise(){
// std::cout << "~Promise" << std::endl;
}
CommonAwaiter<CoTask, EnumAwaiterType::EnumInitial> initial_suspend() {
return {};
};
CommonAwaiter<CoTask, EnumAwaiterType::EnumFinal> final_suspend() noexcept {
return {};
}
// 提供了一种对协程中未捕获的异常的再处理,比如将异常保存下来,实现协程如以下形式 : coroutine().get().catch()
// 这里我们的实现形式决定了,这里直接再次抛出异常就好
void unhandled_exception(){
// try {
std::rethrow_exception(std::current_exception());
// } catch (const std::exception& e) {
// // 输出异常信息
// std::cerr << "Unhandled exception caught in CustomAsyncTask: " << e.what() << std::endl;
// } catch (...) {
// std::cerr << "Unhandled unknown exception caught in CustomAsyncTask!" << std::endl;
// }
}
CoTask get_return_object(){
return CoTask(this);
}
return_type get_value() {
return value_;
}
void return_value(return_type value){
value_ = value;
}
// 该代码写在Promise中的好处是,可以方便阅读代码很容易就能回想出协程最多会返回三个等待体
template<typename T>
CommonAwaiter<CoroutineTask<T>> await_transform(CoroutineTask<T> &&task){
return CommonAwaiter<CoroutineTask<T>>(task.p_);
}
CoTask await_transform(CoTask &&task){
return CommonAwaiter<CoTask>(task.p_);
}
return_type value_;
};
template <typename ReturnType>
struct CoroutineTask{
using return_type = ReturnType;
using promise_type = Promise<CoroutineTask>;
CoroutineTask(const CoroutineTask &other) = delete;
CoroutineTask(const CoroutineTask &&other) = delete;
CoroutineTask& operator=(const CoroutineTask&) = delete;
CoroutineTask& operator=(const CoroutineTask&&) = delete;
CoroutineTask(promise_type* promise) {
p_ = promise;
}
promise_type *p_ = nullptr;
};
CoroutineTask<u_int64_t> second_coroutine(){
co_return 3;
}
CoroutineTask<float> third_coroutine(){
co_return 3.1;
}
CoroutineTask<char> first_coroutine(){
uint64_t num = co_await second_coroutine();
std::cout << "second_coroutine result is : " << num << std::endl;
float num2 = co_await third_coroutine();
std::cout << "third_coroutine result is : " << num2 << std::endl;
co_return 'b';
}
void do_work() {
while (1)
{
std::lock_guard<std::mutex> g(m);
for(auto task : g_work_queue){
task->completed();
g_resume_queue.push_back(task);
}
g_work_queue.clear();
}
}
void run_event_loop(){
std::vector<std::shared_ptr<async_task_base>> g_raw_work_queue_tmp;
std::vector<std::shared_ptr<async_task_base>> g_event_loop_queue_temp;
while(1){
g_raw_work_queue_tmp.clear();
g_event_loop_queue_temp.clear();
{
g_event_loop_queue_temp.swap(g_event_loop_queue);
std::lock_guard<std::mutex> g(m);
g_raw_work_queue_tmp.swap(g_resume_queue);
}
// 优先恢复耗时任务
for(auto &task : g_raw_work_queue_tmp){
task->resume();
}
for(auto task : g_event_loop_queue_temp){
task->resume();
}
}
}
void test_func(){
first_coroutine();
}
int main(){
test_func();
std::thread work_thread(do_work);
run_event_loop();
return 0;
}
代码分析
Promise
unhandled_exception
unhandled_exception 的作用是是用来对协程中未捕获的异常再处理。在一些实现协程使用方式为 **coroutine().get().catch()**的架构中,会把未捕获的异常暂存下来,待恢复的时候再抛出。我选择直接抛出异常,因为出现未捕获的异常时,协程也会提前结束,这时reume的结果是未定义的,所以我觉得在resume之前抛出异常有有必要的。
await_transform
await_transform的作用和重载运算符co_await是一样的,在co_await一个协程时,会转换CoroutineTask为一个awaiter。使用await_transform的优势是,所有等待体的返回时机,都在promise定义出来,方便代码阅读。
这里我们需要注意的是该await_transform需要定义为模板函数,而不能用Promise的类型参数CoTask,作为传入参数类型。
修改代码如下
编译一下,我们发现报错了
这里我们再结合代码理解下
根据报错信息和调用顺序,我们可以得出以下结论:
当前位于CoroutineTask的写成体中,所以对应的promise类型是promise<CoroutineTask>,
这时实例化的await_transform 实际上是 CoroutineTask await_transform (CoroutineTask&&task),而这时await_transform 操作的是协程second_coroutine,协程类型是CoroutineTask<u_int64_t> 类型不一致,所以会出现上面的报错。
CoroutineTask
协程类CoroutineTask要保存什么?
这里只保存了promise的指针,原因如下:
协程和用户代码交互是通过awaiter对象,由于返回值是通过return_value保存在协程promise中的,我们需要在awaiter从promise获取返回的值,所以需要在awaiter中保存promise的指针,那promise的指针从哪来呢?awaiter是在await_transform中使用CoroutineTask初始化的,而我们又知道CoroutineTask是由promise 调用 get_return_object创建的。所以我们在创建CoroutineTask时,将promise的指针保存进去, 这样awaiter就能够通过CoroutineTask作为中介得到promise的指针啦!
CommonAwaiter
其实讲到这里,CommonAwaiter就没多少能讲的东西了。
awaiter使用偏特化,根据不同枚举,特化了三个版本。来控制协程的基本行为:即创建时不挂起,能够有机会被编译器优化为inline;用户代码挂起能够返回任意co_return返回的值;结束挂起,参与调度销毁。对了还有一个问题协程句柄为什么保存在awaiter中而不是promise中。在我看来awaiter就代表了每个挂起点,所以将couroutine_handle保存在awaiter中,couroutine_handle很小所以不考虑内存开销
运行结果
最后我们运行下代码,完美运行。
这里就不阐述流程了,下一篇会将二、三两节的代码合并起来,集中阐述下流程和汇总下重要的知识点。