C++ 协程
C++20 引入了协程(Coroutines),协程是一种可以在执行过程中暂停和恢复的函数,允许在特定点挂起(suspend)执行,并将控制权交还给调用者,稍后可以从暂停点恢复执行。
应用场景
与传统的函数调用不同,协程可以在不丢失上下文的情况下暂停,适合以下场景:
1.异步编程:例如处理 I/O 操作(如网络请求、文件读写)时,避免阻塞线程。
2.生成器:逐步生成数据序列,适合处理大数据流或惰性求值。
3.协作式多任务:多个任务协作运行,共享线程资源。
关键字
C++ 的协程是通过编译器支持的底层机制实现的,主要依赖三个关键字:
1.co_await
:暂停协程并等待某个异步操作完成。
2.co_yield
:暂停协程并向调用者返回一个值(用于生成器)。
3.co_return
:结束协程并返回结果。
关键组件
C++ 协程的实现依赖于几个关键组件,这些组件共同定义了协程的行为:
1.Promise 对象(promise_type
):每个协程都有一个关联的
promise_type
,它定义了协程的行为和返回值。promise_type
是一个类,通常包含以下方法:
get_return_object()
:定义协程的返回值对象(通常是一个协程句柄或自定义类型)。
initial_suspend()
:决定协程在启动时是否立即挂起(返回
std::suspend_always
或
std::suspend_never
)。
final_suspend()
:决定协程在结束时是否挂起。
return_void()
或
return_value(T)
:处理协程的返回值。
yield_value(T)
:处理通过
co_yield
返回的值(生成器场景)。
unhandled_exception()
:处理协程中未捕获的异常。
2.协程句柄(std::coroutine_handle
):std::coroutine_handle
是一个模板类,用于管理协程的状态和生命周期。它可以:恢复协程(resume()
),检查协程是否完成(done()
),销毁协程(destroy()
)。
3.协程框架:协程的执行需要一个框架来管理暂停和恢复。C++20
标准库没有提供内置的协程调度器,因此开发者通常需要:手动管理协程句柄,使用第三方库(如
liburing
、boost::asio
)提供的事件循环或调度器。
4.暂停点:暂停点由 co_await
或 co_yield
定义:
co_await expr
:暂停协程,等待表达式
expr
(通常是一个等待器,awaitable
)完成。
co_yield expr
:暂停协程并向调用者返回一个值。
暂停时,协程的状态(包括局部变量)被保存在堆上,称为协程帧(coroutine frame)。
工作原理
C++ 协程的实现依赖编译器将协程函数转换为状态机(state machine)。其工作原理如下:
1.函数转换:当编译器遇到
co_await
、co_yield
或 co_return
时,它将协程函数拆分为多个状态,每个状态对应一个暂停点或结束点。
2.协程帧分配:协程的局部变量和状态存储在堆上的协程帧中,确保暂停后状态不丢失。
3.状态机执行:协程在暂停点挂起时,返回控制权给调用者;恢复时,从暂停点继续执行。
4.生命周期管理:协程帧的分配和销毁由
promise_type
和 std::coroutine_handle
管理。
示例
生成器示例(使用
co_yield
)
这个示例实现一个生成整数序列的协程:
1 |
|
说明:Generator
是一个自定义协程类型,存储通过
co_yield
返回的值。promise_type
的
yield_value
方法将每次 co_yield
的值保存到
values
向量中。main
函数调用生成器并打印结果。
异步示例(使用
co_await
)
以下是一个模拟异步任务的协程,假设有一个简单的等待器:
1 |
|
说明:Awaiter
模拟一个异步操作(如网络 I/O),在 100ms
后恢复协程。co_await Awaiter{}
暂停协程,直到异步操作完成。main
启动多个异步任务,实际场景中需要事件循环来管理。
协程的优缺点
优点
1.高效性:协程在用户态管理暂停和恢复,避免了线程切换的开销。
2.灵活性:C++ 协程是底层机制,允许开发者自定义调度和行为。
3.异步编程简化:与回调或 std::future
相比,协程代码更直观,类似同步代码。
4.生成器支持:适合处理流式数据或惰性求值。
缺点
1.复杂性:需要手动定义 promise_type
和调度逻辑,学习曲线陡峭。
2.标准库支持不足:C++20 没有内置事件循环或调度器,需依赖第三方库。
3.内存管理:协程帧在堆上分配,可能增加内存开销。
4.调试困难:状态机转换和协程帧管理可能导致调试复杂。
性能优化
避免在协程中分配过多内存,合理设计暂停点。