文章目录
- 从实战日志读懂C++20协程:异步操作的优雅实现
- 零、代码和日志
- 代码
- 日志
- 一、代码场景与核心目标
- 二、协程核心组件拆解
- 1. 可等待对象(AsyncOperation)
- 2. Promise类型(Task::promise_type)
- 3. 协程句柄(coroutine_handle)
- 三、从日志看协程完整执行流程
- 阶段1:协程创建与初始化
- 阶段2:第一次co_await(协程暂停)
- 阶段3:异步操作执行与协程恢复
- 阶段4:第二次co_await(重复暂停-恢复)
- 阶段5:第三次co_await(无暂停)
- 阶段6:协程收尾与资源释放
- 四、关键知识点总结
- 五、协程的优势与适用场景
从实战日志读懂C++20协程:异步操作的优雅实现
协程(Coroutine)是C++20引入的核心特性,它打破了传统函数“一次性执行完毕”的执行模型,允许函数在执行过程中暂停(suspend)和恢复(resume),是实现异步编程、生成器等场景的利器。本文将结合一段带详细日志的协程实战代码,从执行流程、核心组件、关键机制三个维度,拆解C++协程的工作原理。
零、代码和日志
代码
#include<iostream>#include<coroutine>#include<thread>#include<chrono>// 增强的可等待对象,添加更多日志structAsyncOperation{intvalue;boolready=false;constchar*name;AsyncOperation(intv,boolr,constchar*n=""):value(v),ready(r),name(n){std::cout<<"["<<name<<"] AsyncOperation 构造: value="<<value<<", ready="<<ready<<" (线程: "<<std::this_thread::get_id()<<")"<<std::endl;}boolawait_ready()constnoexcept{std::cout<<"["<<name<<"] await_ready() 调用, 返回: "<<ready<<" (线程: "<<std::this_thread::get_id()<<")"<<std::endl;returnready;}voidawait_suspend(std::coroutine_handle<>handle)noexcept{std::cout<<"["<<name<<"] await_suspend() 调用, 协程句柄: "<<handle.address()<<" (线程 : "<<std::this_thread::get_id()<<") "<<std::endl;// 启动异步操作std::thread([this,handle,name=this->name](){std::cout<<"["<<name<<"] 异步线程开始 (线程: "<<std::this_thread::get_id()<<")"<<std::endl;std::this_thread::sleep_for(std::chrono::seconds(1));this->ready=true;std::cout<<"["<<name<<"] 异步操作完成, 准备恢复协程"<<" (线程: "<<std::this_thread::get_id()<<")"<<std::endl;// 恢复协程执行handle.resume();std::cout<<"["<<name<<"] 异步线程结束"<<" (线程: "<<std::this_thread::get_id()<<")"<<std::endl;}).detach();std::cout<<"["<<name<<"] await_suspend() 返回, 协程即将挂起"<<" (线程: "<<std::this_thread::get_id()<<")"<<std::endl;}intawait_resume()noexcept{std::cout<<"["<<name<<"] await_resume() 调用, 返回结果: "<<value<<" (线程: "<<std::this_thread::get_id()<<")"<<std::endl;returnvalue;}};structTask{structpromise_type{intcurrent_value;Taskget_return_object(){std::cout<<"promise_type::get_return_object() 调用"<<std::endl;returnTask{std::coroutine_handle<promise_type>::from_promise(*this)};}std::suspend_neverinitial_suspend()noexcept{std::cout<<"promise_type::initial_suspend() 调用 - 立即开始执行"<<std::endl;return{};}std::suspend_alwaysfinal_suspend()noexcept{std::cout<<"promise_type::final_suspend() 调用 - 协程结束"<<std::endl;return{};}voidunhandled_exception(){std::cout<<"promise_type::unhandled_exception() 调用"<<std::endl;}voidreturn_void(){std::cout<<"promise_type::return_void() 调用 - 协程正常返回"<<std::endl;}};std::coroutine_handle<promise_type>handle;Task(std::coroutine_handle<promise_type>h):handle(h){std::cout<<"Task 对象构造, 句柄: "<<handle.address()<<std::endl;}~Task(){if(handle){std::cout<<"Task 析构, 销毁协程句柄"<<std::endl;handle.destroy();}}};Taskdetailed_async_example(){std::cout<<"=== 协程函数开始执行 ==="<<" (线程: "<<std::this_thread::get_id()<<")"<<std::endl;std::cout<<"\n--- 第一次 co_await ---"<<" (线程: "<<std::this_thread::get_id()<<")"<<std::endl;intresult1=co_awaitAsyncOperation{100,false,"操作1"};std::cout<<"第一次 co_await 后继续执行, 结果: "<<result1<<" (线程: "<<std::this_thread::get_id()<<")"<<std::endl;std::cout<<"\n--- 第二次 co_await ---"<<" (线程: "<<std::this_thread::get_id()<<")"<<std::endl;intresult2=co_awaitAsyncOperation{200,false,"操作2"};std::cout<<"第二次 co_await 后继续执行, 结果: "<<result2<<" (线程: "<<std::this_thread::get_id()<<")"<<std::endl;std::cout<<"\n--- 第三次 co_await (就绪状态) ---"<<" (线程: "<<std::this_thread::get_id()<<")"<<std::endl;intresult3=co_awaitAsyncOperation{300,true,"操作3"};std::cout<<"第三次 co_await 后继续执行, 结果: "<<result3<<" (线程: "<<std::this_thread::get_id()<<")"<<std::endl;std::cout<<"\n=== 协程函数执行完成 ==="<<" (线程: "<<std::this_thread::get_id()<<")"<<std::endl;}intmain(){std::cout<<"主函数开始 (线程: "<<std::this_thread::get_id()<<")"<<std::endl;{std::cout<<"\n********** 创建协程任务 **********"<<" (线程: "<<std::this_thread::get_id()<<")"<<std::endl;autotask=detailed_async_example();std::cout<<"协程任务已创建,控制权返回主函数"<<" (线程: "<<std::this_thread::get_id()<<")"<<std::endl;// 给异步操作时间完成std::cout<<"\n主线程等待 3 秒..."<<" (线程: "<<std::this_thread::get_id()<<")"<<std::endl;std::this_thread::sleep_for(std::chrono::seconds(3));std::cout<<"主线程等待结束"<<" (线程: "<<std::this_thread::get_id()<<")"<<std::endl;}// task 析构std::cout<<"\n主函数结束"<<" (线程: "<<std::this_thread::get_id()<<")"<<std::endl;return0;}日志
主函数开始(线程:132140388288320)********** 创建协程任务 **********(线程:132140388288320)promise_type::get_return_object()调用 Task 对象构造, 句柄: 0x5e3b8cf602c0 promise_type::initial_suspend()调用 - 立即开始执行===协程函数开始执行===(线程:132140388288320)--- 第一次 co_await ---(线程:132140388288320)[操作1]AsyncOperation 构造:value=100,ready=0(线程:132140388288320)[操作1]await_ready()调用, 返回:0(线程:132140388288320)[操作1]await_suspend()调用, 协程句柄: 0x5e3b8cf602c0(线程:132140388288320)[操作1]await_suspend()返回, 协程即将挂起(线程:132140388288320)协程任务已创建,控制权返回主函数(线程:132140388288320)主线程等待3秒...(线程:132140388288320)[操作1]异步线程开始(线程:132140387796544)[操作1]异步操作完成, 准备恢复协程(线程:132140387796544)[操作1]await_resume()调用, 返回结果:100(线程:132140387796544)第一次 co_await 后继续执行, 结果:100(线程:132140387796544)--- 第二次 co_await ---(线程:132140387796544)[操作2]AsyncOperation 构造:value=200,ready=0(线程:132140387796544)[操作2]await_ready()调用, 返回:0(线程:132140387796544)[操作2]await_suspend()调用, 协程句柄: 0x5e3b8cf602c0(线程:132140387796544)[操作2]await_suspend()返回, 协程即将挂起(线程:132140387796544)[操作1]异步线程结束(线程:132140387796544)[操作2]异步线程开始(线程:132140303910464)[操作2]异步操作完成, 准备恢复协程(线程:132140303910464)[操作2]await_resume()调用, 返回结果:200(线程:132140303910464)第二次 co_await 后继续执行, 结果:200(线程:132140303910464)--- 第三次 co_await(就绪状态)---(线程:132140303910464)[操作3]AsyncOperation 构造:value=300,ready=1(线程:132140303910464)[操作3]await_ready()调用, 返回:1(线程:132140303910464)[操作3]await_resume()调用, 返回结果:300(线程:132140303910464)第三次 co_await 后继续执行, 结果:300(线程:132140303910464)===协程函数执行完成===(线程:132140303910464)promise_type::return_void()调用 - 协程正常返回 promise_type::final_suspend()调用 - 协程结束[操作2]异步线程结束(线程:132140303910464)主线程等待结束(线程:132140388288320)Task 析构, 销毁协程句柄 主函数结束(线程:132140388288320)一、代码场景与核心目标
示例代码模拟了三个异步操作的执行流程:前两个异步操作需要耗时1秒完成(非就绪状态),第三个操作处于就绪状态无需等待。通过在关键节点打印日志(包括线程ID、函数调用、状态变化),我们能清晰看到协程“暂停-恢复”的完整生命周期,以及协程与线程的交互关系。
二、协程核心组件拆解
要理解C++协程,首先要掌握三个核心组件:可等待对象(Awaitable)、协程句柄(coroutine_handle)、Promise类型。
1. 可等待对象(AsyncOperation)
可等待对象是协程暂停/恢复的核心载体,必须实现三个核心方法(俗称“await三部曲”):
await_ready():判断是否需要暂停。返回true时,协程不暂停,直接执行await_resume();返回false时,协程暂停并执行await_suspend()。await_suspend(handle):协程暂停时执行的逻辑,接收协程句柄(恢复协程的“钥匙”)。示例中在此启动新线程执行异步操作,耗时1秒后通过handle.resume()恢复协程。await_resume():协程恢复时执行的逻辑,返回异步操作的结果(示例中返回value)。
从日志中能清晰看到非就绪对象(操作1、操作2)和就绪对象(操作3)的执行差异:
// 操作3(就绪状态)的执行日志 [操作3] await_ready() 调用, 返回: 1 [操作3] await_resume() 调用, 返回结果: 300操作3的await_ready()返回true,跳过await_suspend()直接执行await_resume(),协程无暂停。
2. Promise类型(Task::promise_type)
每个协程都必须关联一个Promise对象,它负责协程的创建、暂停策略、返回值/异常处理。示例中Promise类型实现了5个关键方法:
get_return_object():创建协程的返回对象(示例中为Task),协程启动时第一个被调用。initial_suspend():协程启动后是否立即暂停。示例返回std::suspend_never,表示协程启动后立即执行。final_suspend():协程执行完毕后是否暂停。示例返回std::suspend_always,确保协程结束后不立即销毁,等待句柄手动销毁。return_void():协程无返回值时的收尾逻辑(对应co_return或协程正常结束)。unhandled_exception():处理协程执行中未捕获的异常。
3. 协程句柄(coroutine_handle)
协程句柄是操作协程的“手柄”,可以理解为指向协程执行上下文的指针。核心作用是:
- 通过
handle.resume()恢复暂停的协程; - 通过
handle.destroy()销毁协程的执行上下文(避免内存泄漏); handle.address()可获取句柄地址,用于日志追踪。
示例中Task类封装了句柄,并在析构函数中调用handle.destroy(),保证协程资源正确释放。
三、从日志看协程完整执行流程
结合日志的时间线和线程ID,我们可以把协程执行分为6个关键阶段:
阶段1:协程创建与初始化
主函数开始 (线程: 132140388288320) ********** 创建协程任务 ********** (线程: 132140388288320) promise_type::get_return_object() 调用 Task 对象构造, 句柄: 0x5e3b8cf602c0 promise_type::initial_suspend() 调用 - 立即开始执行 === 协程函数开始执行 === (线程: 132140388288320)- 调用
detailed_async_example()(协程函数)时,首先创建promise_type对象; - 执行
get_return_object()创建Task对象,封装协程句柄; initial_suspend()返回std::suspend_never,协程不暂停,立即进入函数体执行。
阶段2:第一次co_await(协程暂停)
--- 第一次 co_await --- (线程: 132140388288320) [操作1] AsyncOperation 构造: value=100, ready=0 (线程: 132140388288320) [操作1] await_ready() 调用, 返回: 0 (线程: 132140388288320) [操作1] await_suspend() 调用, 协程句柄: 0x5e3b8cf602c0 (线程 : 132140388288320) [操作1] await_suspend() 返回, 协程即将挂起 (线程: 132140388288320) 协程任务已创建,控制权返回主函数 (线程: 132140388288320)co_await AsyncOperation{100, false, "操作1"}触发可等待对象的构造;await_ready()返回false,协程需要暂停;await_suspend()启动新线程执行异步操作,随后协程暂停,控制权返回主函数。
阶段3:异步操作执行与协程恢复
主线程等待 3 秒... (线程: 132140388288320) [操作1] 异步线程开始 (线程: 132140387796544) [操作1] 异步操作完成, 准备恢复协程 (线程: 132140387796544) [操作1] await_resume() 调用, 返回结果: 100 (线程: 132140387796544) 第一次 co_await 后继续执行, 结果: 100 (线程: 132140387796544)- 主线程进入等待,异步线程(132140387796544)执行耗时操作;
- 异步操作完成后,调用
handle.resume()恢复协程,协程从暂停点继续执行; await_resume()返回结果,协程执行后续逻辑(打印结果)。
阶段4:第二次co_await(重复暂停-恢复)
第二次co_await的流程与第一次完全一致:协程在异步线程中暂停,新的异步线程执行操作后恢复协程,核心差异是恢复后的执行线程变为新的异步线程(132140303910464)。
阶段5:第三次co_await(无暂停)
[操作3] await_ready() 调用, 返回: 1 (线程: 132140303910464) [操作3] await_resume() 调用, 返回结果: 300 (线程: 132140303910464) 第三次 co_await 后继续执行, 结果: 300 (线程: 132140303910464)await_ready()返回true,协程不暂停,直接执行await_resume()并获取结果,体现了可等待对象“按需暂停”的灵活性。
阶段6:协程收尾与资源释放
=== 协程函数执行完成 === (线程: 132140303910464) promise_type::return_void() 调用 - 协程正常返回 promise_type::final_suspend() 调用 - 协程结束 主线程等待结束 (线程: 132140388288320) Task 析构, 销毁协程句柄- 协程函数执行完毕后,调用
return_void()完成收尾; final_suspend()暂停协程,等待句柄销毁;Task对象析构时调用handle.destroy(),释放协程的执行上下文。
四、关键知识点总结
- 协程与线程的关系:协程本身不绑定线程,暂停的协程可以在任意线程中恢复(示例中协程分别在主线程、操作1线程、操作2线程中执行),这是协程轻量级的核心原因;
- 可等待对象的核心作用:通过
await_ready()决定是否暂停,await_suspend()处理暂停逻辑(如启动异步操作),await_resume()返回结果,是协程与异步操作的桥梁; - Promise类型的职责:管理协程的生命周期,包括创建返回对象、控制初始/最终暂停策略、处理返回值和异常,是协程的“幕后管理者”;
- 资源管理:协程句柄需手动销毁(示例中在
Task析构函数中完成),否则会导致内存泄漏。
五、协程的优势与适用场景
对比传统的回调函数、线程池,协程的优势在于:
- 代码可读性:异步逻辑以“同步写法”实现,避免回调嵌套(回调地狱);
- 性能:协程暂停/恢复的开销远低于线程切换,适合高并发场景;
- 灵活性:可按需暂停/恢复,适配异步IO、定时任务、生成器等场景。
C++20协程虽上手有一定门槛,但掌握其核心组件和执行流程后,能显著简化异步编程的复杂度。本文的实战日志为理解协程提供了直观视角,建议结合代码调试,观察每一步的执行顺序和线程变化,加深对协程的理解。