【协程的使用-C++20】
·
协程的使用-C++20
一、协程使用(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 new 和 operator 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 总结协程的执行顺序
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 参考链接
更多推荐



所有评论(0)