c++20协程详解(三)

前言

前面两节我们已经能够实现一个可用的协程框架了。但我们一定还想更深入的了解协程,于是我们就想尝试下能不能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很小所以不考虑内存开销

运行结果

最后我们运行下代码,完美运行。

这里就不阐述流程了,下一篇会将二、三两节的代码合并起来,集中阐述下流程和汇总下重要的知识点。

相关推荐
来世做春风嘛8 分钟前
第二十一章 网络编程
服务器·网络·php
性野喜悲29 分钟前
vue通过后台返回的数字显示不同的文字内容,多个内容用、隔开
前端·javascript·vue.js
武汉前端开发蓝风1 小时前
前端Debugger时复制的JS对象字符转JSON对象
前端·javascript·json·debugger
爱编程的鱼1 小时前
HTML如何在图片上添加文字
前端·javascript·html
顶顶年华正版软件官方1 小时前
关键帧功能怎么使用 关键帧控制视频特效怎么用 会声会影视频剪辑软件教程
前端·javascript·音视频·学习方法·关键帧·会声会影教程·视频剪辑软件
来之梦1 小时前
uniapp自定义富文本现实组件(支持查看和收起)
前端·javascript·uni-app
森叶2 小时前
Electron开发 - 如何在主进程Main中让node-fetch使用系统代理
前端·javascript·electron
睿麒2 小时前
前端八股文 说一说样式优先级的规则是什么?
前端
小妖怪的夏天2 小时前
Electron Forge 打包更改打包后图片
前端·javascript·electron
吕彬-前端2 小时前
es6之Proxy实现观察者模式
前端·观察者模式·es6