一、协程使用(C++20)

  • 协程即可以暂停的函数,该特性可以实现在异步的场景下实现同步的代码编写样式,协程特别适合惰性计算和IO密集多任务的异步编程,协程在C++中的实现方法为编译器会为每个协程创建自己的协程帧,协程帧内保存有当前的函数的状态,以便暂停和恢复协程函数的运行,此协程帧的数据保存在堆,因此C++的协程是无栈协程

1.1 coroutine_handle-协程句柄管理对象

  • 当编译器遇到一个协程对象的时候,会首先建立一个协程管理对象,既然是编译器要指定调用,那么就需要实现编译器指定的函数名和相关类,编译器要求在自己协程管理对象内,实现promise_type类,用以实现对协程运行状态的控制

  • 判断函数为协程函数的条件为出现co_return co_yield co_awit中的任何一个

class Task{ // Task 类模板,表示一个协程任务,负责管理协程句柄的生命周期
public:
    struct promise_type{ // promise_type 结构体内的函数基本都不能更改名字,要和编译器要求的一致,以下函数是必须要实现的
    public:
        Task get_return_object(){  // 2.调用get_return_object创建外部task对象,该对象会作为结果返回
            return Task(std::coroutine_handle<promise_type>::from_promise(*this)); 
        };
        // std::suspend_always 表示挂起
        // std::suspend_never 表示不挂起
        std::suspend_always initial_suspend(){ return {}; }; // 3.根据返回结果判断协程刚创建是立刻运行还是惰性挂起
        std::suspend_always final_suspend() noexcept { return {}; };  // 7.函数执行结束根据返回结果是挂起等待销毁,还是立即销毁,此时协程的done是完成状态
        void return_void(){}; // 无返回值的时候,调用 6.调用return_void或return_value,但是这两个都没有返回值,而是把值存储在promise里
        // void return_value(int && v){ // corutun 返回的是右值引用
        //     std::cout << "return_value called_&&" << std::endl;
        //     value = std::move(v); // 触发移动构造
        // };
        void unhandled_exception(){}; // 当出现异常且没有被捕获的时候,会调用该函数进行异常的兜底处理
    };
    int value;
    Task(std::coroutine_handle<promise_type> handle) : handle(handle){};
    ~Task(){ // 析构的时候,销毁协程句柄,注意final_suspend自动销毁的话,此处不要在一次销毁了
        if(handle){
            std::cout << "destroying coroutine" << std::endl;
            handle.destroy();
        };
    };
	
    std::coroutine_handle<promise_type> handle; //  此处存储的正是内部promise_type对象,如果想要获取promis_type内的的值,可以使用handle.promise(),如果是std::coroutine_handle<> handle,则无法获取promis_type内的值,只可以用来管理协程
};


Task test(){
    // 5.开始执行
    std::cout << "test coroutine_qqq" << std::endl;
    std::vector<int> ve = {1,2,3,4,5};
    co_return ; // 6.遇到co_return,调用return_void 或return_value 
};

int main(){
    Task handle  = test(); // 1.test为协程任务,执行此行编译器开始做协程相关的流程,首先创建promise_type对象
    handle.handle.resume(); // 4.因为初始挂起,所以要手动恢复执行
    return 0;
}

1.2 协程帧的内存手动申请

在 C++20 协程中,当一个协程被创建时,编译器默认会通过 new 为其分配协程帧(coroutine frame)所需的内存;当协程执行完毕并销毁时,则通过 delete 释放该内存。

这种默认行为对于大多数普通应用场景是足够安全且高效的。然而,在某些高性能或高并发场景下(例如每秒需要创建成千上万个短生命周期协程的服务器程序),频繁调用 new/delete 会带来显著的性能开销:不仅增加了内存分配/回收的 CPU 时间,还可能引发内存碎片,进而成为系统瓶颈。

为了解决这个问题,C++ 允许开发者通过自定义协程的内存分配策略(例如重载 operator newoperator delete)来接管协程帧的内存管理。这样可以大幅降低分配成本、提升内存访问局部性,并使性能表现更加可预测。

class Task{
    struct promise_type{
        // 其他函数已省略
        static void * operator new(size_t size){ // 注意为静态函数,因为该函数需要没有 promise_type 对象的实例下调用
            // 高性能场景此处为申请内存池
            std::cout << "promise_type operator new called, size: " << size << std::endl;
            return ::operator new(size);
        };

        static void operator delete (void * prt ){
            // 高性能场景此次为释放内存池
            std::cout << "promise_type operator delete called" << std::endl;
            ::operator delete(prt);
        };
    }
}

1.3 co_yield 生成迭代器适合惰性计算

// 惰性计算
template <typename U >
class Task{ // Task 类模板,表示一个协程任务,负责管理协程句柄的生命周期
public:
    struct promise_type{ // promise_type 结构体内的函数基本都不能更改名字,要和编译器要求的一致
    public:
        Task get_return_object(){
            std :: cout << "get_return_object called" << std :: endl;
            return Task(std::coroutine_handle<promise_type>::from_promise(*this));
        };
        std::suspend_always initial_suspend(){ std::cout << "initial_suspend called" << std::endl; return {}; };
        std::suspend_always final_suspend() noexcept { std::cout << "final_suspend called" << std::endl; return {}; };
        void return_void(){ std::cout << "return_void called" << std::endl;}; // 无返回值的时候,调用
        void unhandled_exception(){ std::cout << "unhandled_exception called" << std::endl; }; // 当出现异常且没有被捕获的时候,会调用该函数进行异常的兜底处理
        std::suspend_always yield_value(U && v){ //yield_value 为co_yield 关键字会调用的函数,v是co_yield传入的值
            y_value = v;
            return {};
        };
        U y_value;
    };
    Task(std::coroutine_handle<promise_type> handle) : handle(handle){};
    ~Task(){
        if(handle){
            handle.destroy();
        };
    };

    void resume(){
        if(handle && !handle.done()){
            handle.resume();
        }
    };

 
    promise_type get_handle_promise(){
        return handle.promise();
    };

    U && next(){ // 外层可以实现一个next函数,主要为恢复协程函数运行和获取协程yiled的结果
        if(handle && !handle.done()){
            handle.resume();
            return std::move(this->get_handle_promise().y_value);
        };
    };

    bool done(){
        if (handle){
            return handle.done();
        };
        return false;
    };
private:
    std::coroutine_handle<promise_type> handle;
};


Task<int> test_yield(){ // 惰性计算,每次调用next()才会继续执行协程
    int i = 0;
    while(true){
        co_yield i * i++;
    }
};

int main(){
    Task<int> handle  = test_yield();
    for (int j = 0 ; j < 10 ; j++){
        int v = handle.next();
        std::cout << "yielded value: " << v << std::endl;
    }
    return 0;
}

1.4 co_await 可等待对象实现 适合异步多任务

  • co_await 后面需要是一个可等待对象,可等待对象要求在对象内实现下列函数,用来规范协程遇到co_await是不是要挂起,挂起前要做什么,恢复执行后要做什么
struct Awaiter
{
    // 是否继续执行
    // true 表示继续执行,然后执行 await_resume() 返回结果
    // false 表示挂起,然后执行 await_suspend() 进行挂起前的操作
    bool await_ready();
    
    // await_suspend 支持void/bool/handle三种返回情况
    // 返回 void:执行后挂起
    // 返回 bool:true 表示执行完后挂起,false 表示执行完后不挂起,继续执行 await_resume 函数
    // 返回 handle:表示执行完后,继续恢复执行 handle 关联的协程
    void/bool/handle await_suspend(std::coroutine_handle<> handle); // 如果需要在await_suspend内获取promis_type的值,需要传入std::coroutine_handle<promise_type> 

    // 协程恢复后执行,返回 co_await 的结果
    T await_resume();
};
// 多任务的简单实现
// 协程调度的封装

struct Scheduler{

public:
    void add_task(std::coroutine_handle<> t){ // 初始用来添加协程任务
        push_queue(t);
        task_number++;
    };
    void push_queue(std::coroutine_handle<> t){ // 用来将就绪的协程句柄放入任务队列
        std::lock_guard<std::mutex> lg(mtx);
        task_queue.push(t); 
        cv.notify_one();
    };
    std::coroutine_handle<> pop_queue(){ // 用来从任务队列中取出就绪的协程句柄
        std::unique_lock<std::mutex> lock(mtx);
        if (task_queue.empty()){ // 为空阻塞等待唤醒
            cv.wait(lock);   
        };
        auto t = task_queue.front();
        task_queue.pop();
        return t;
    };   

    void run(){
        while (task_number > 0){ // 所有任务结束,跳出循环
            auto t = pop_queue();
            if(!t){
                continue;
            }
            t.resume(); // 恢复协程,协程执行结束或下一个await会回到此时,向下执行
            if (t.done()){
                task_number--;
                t.destroy(); // 此处销毁协程句柄,那么析构函数就不要在此销毁了
            }
        }
    };


private:
    std::queue<std::coroutine_handle<>> task_queue;
    std::mutex mtx;
    std::condition_variable cv;
    size_t task_number{0};
};

Scheduler scheduler;

struct Task{
    struct promise_type{
        Task get_return_object(){
           return Task(std::coroutine_handle<promise_type>::from_promise(*this));
        };
        std::suspend_always initial_suspend(){ return {}; };
        std::suspend_always final_suspend() noexcept { return {}; };
        void return_void(){  };
        void unhandled_exception(){  };
          
    };

Task(std::coroutine_handle<promise_type> h) : handle(h){};

operator std::coroutine_handle<>(){ // 实现一下task 到 coroutine_handle 的转换
    return handle;
}

private:
    std::coroutine_handle<promise_type> handle;
};

struct Await_sleep_for{ // 封装sleep_for,使其成为一个可等待对象,避免协程阻塞
    int sleep_for_time{0};
    std::shared_ptr<std::future<void>> fut;
    bool await_ready() {  return false;  } ;
    void await_suspend(std::coroutine_handle<> handle){
        fut =std::make_shared<std::future<void>>(std::async(std::launch::async, [=](){ 
            // 模拟事件的处理,一般来说这里应该由系统的API处理,比如网络IO epoll,系统定时器等
            std::this_thread::sleep_for(std::chrono::seconds(sleep_for_time));
            scheduler.push_queue(handle);
            // handle.resume();
        })); // async 返回的future,在该future析构时会等待任务完成,所以这里不可以使用临时变量保存,否则会在此阻塞等待任务结束
        
        // std::thread([=](){ 
        //     std::this_thread::sleep_for(std::chrono::seconds(sleep_for_time));
        //     scheduler.push_queue(handle);
        //     // handle.resume();
        // }).detach(); // 使用detach的线程不需要等待线程结束
    };
    void  await_resume() {};
};

Task task(int sleep_for_time){
    std::cout << "task" << sleep_for_time << " start" << std::endl;
    co_await Await_sleep_for{sleep_for_time};
    std::cout << "task" << sleep_for_time << " end" << sleep_for_time << " seconds" << std::endl;
}


int main(){
    auto start = std::chrono::steady_clock::now();
    for (int i = 5 ; i > 0 ; i--){ // 添加需要执行的任务到调度器
        auto t = task(i);
        scheduler.add_task(t);
    }
    scheduler.run(); // 启动调度器,运行所有任务
    auto end = std::chrono::steady_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
    std::cout << "程序耗时:" << duration.count() << "毫秒" << std::endl;
}

1.5 总结协程的执行顺序

协程句柄 协程任务 主函数 协程句柄 协程任务 主函数 auto handle = task() 创建promis_type 调用get_return_object,创建外层管理对象 initial_suspend,判断是否要执行协程 执行,协程任务继续 遇到co_yield 调用yield_value,挂起,返回主函数 获取值,恢复协程运行 遇到co_await 调用await_ready,是否挂起 挂起,执行await_suspend 协程挂起,回到主函数 恢复协程运行 调用await_resume,返回函数结果 遇到co_return 调用return_void或return_value 调用final_suspend是否销毁 如果有返回值,调用获取,然后销毁协程

1.6 协程编译的Cmake文件

cmake_minimum_required(VERSION 3.10)
project(cpp_len)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON) # 强制使用所设置的C++标准
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # 生成编译命令文件 compile_commands.json
set(CMAKE_BUILD_TYPE Debug) # Release Debug RelWithDebInfo MinSizeRel 编译模式带调试信息

set(DCMAKE_C_COMPILER clang) # 设置C编译器为clang
set(DCMAKE_CXX_COMPILER clang++)  # 设置C++编译器为clang++

# 对于 GCC
# if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
#     add_compile_options(-fcoroutines) # GCC 启用协程支持
# endif()

# # 对于 Clang
# if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
#     add_compile_options(-fcoroutines-ts) # clang 启用协程支持
# endif()
add_compile_options(-fcoroutines-ts) # clang 启用协程支持 此处为关键

# 添加源文件
# file(GLOB_RECURSE SOURCES src/*.c)

add_executable(code temp.cpp)

# target_link_libraries(code uring)

# # 如需链接库,可在此添加
# target_link_libraries(code pthread)

1.6 参考链接

https://mengbaoliang.cn/archives/131970/#title-2

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐