协程(Coroutine)是一种轻量级的用户级线程,它可以在函数内部暂停执行并在后续恢复,从而实现异步编程、事件驱动和并发控制等功能。C++20 标准中引入了原生协程(co_await、co_yield、co_return)支持,使得协程的使用变得更直观。本文将从原理、实现细节以及一个完整的协程例子三部分来剖析如何在 C++ 中使用协程。
一、协程的基本原理
-
挂起与恢复
- 当协程执行到
co_await、co_yield或co_return时会“挂起”,把当前执行状态(寄存器、栈帧、局部变量等)保存下来,随后返回控制权给调用者。 - 当协程再次被调度(通常是通过
resume())时,会恢复之前保存的状态,从挂起点继续执行。
- 当协程执行到
-
协程句柄(
std::coroutine_handle)- 句柄是协程的管理对象,负责启动、挂起、恢复、销毁协程。
- 通过
coroutine_handle::from_promise(promise)可以从协程的promise_type获得句柄。
-
协程的 Promise 对象
- 每个协程都有一个
promise_type,负责协程的生命周期管理。 promise_type必须实现若干接口,如get_return_object()、initial_suspend()、final_suspend()、return_void()或return_value()等。
- 每个协程都有一个
二、C++20 协程的实现细节
1. promise_type 必须实现的成员
| 成员函数 | 说明 |
|---|---|
get_return_object() |
返回协程可被外部使用的对象,通常返回 `std::coroutine_handle |
| ` 或自定义包装类型 | |
initial_suspend() |
决定协程在起始时是否立即挂起,返回 std::suspend_always 或 std::suspend_never |
final_suspend() |
决定协程在完成时是否挂起,返回 std::suspend_always 或 std::suspend_never |
return_void() / return_value(T) |
处理协程的返回值 |
unhandled_exception() |
处理协程中抛出的异常 |
yield_value(T) |
处理 co_yield 的值,返回 std::suspend_always 或 std::suspend_never |
2. co_await 与 Awaitable 对象
当协程执行 co_await expr 时,编译器会尝试将 expr 转换为 Awaitable 类型。Awaitable 必须至少实现:
| 成员 | 说明 |
|---|---|
await_ready() |
若立即可完成则返回 true |
await_suspend(coroutine_handle) |
若需要挂起则返回 true,并保存协程句柄以供后续恢复 |
await_resume() |
在协程恢复后返回值 |
3. 资源管理
协程的堆栈是由编译器自动管理的,但协程内部所分配的资源(如文件句柄、网络连接)需要在 final_suspend() 或 return_value() 中手动释放。常用做法是将资源包装在 RAII 对象里,确保在协程结束时自动析构。
三、完整示例:异步文件读取协程
下面给出一个使用标准库实现的简单异步文件读取协程示例,演示如何使用 co_await 读取文件块并逐块处理。
#include <coroutine>
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <optional>
#include <thread>
#include <chrono>
// Awaitable:模拟异步读取
struct AsyncRead {
std::ifstream &file;
std::size_t size;
std::vector <char> buffer;
std::coroutine_handle<> caller; // 被挂起的协程句柄
AsyncRead(std::ifstream &f, std::size_t sz)
: file(f), size(sz), buffer(sz) {}
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) {
caller = h;
// 异步读取(这里用线程模拟)
std::thread([this]() {
file.read(buffer.data(), size);
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟延迟
caller.resume(); // 读取完成后恢复协程
}).detach();
}
std::optional<std::vector<char>> await_resume() noexcept {
if (file.gcount() == 0) return std::nullopt;
return buffer;
}
};
// 协程函数:读取文件并打印
std::coroutine_handle<> read_file_async(const std::string &path) {
std::ifstream file(path, std::ios::binary);
if (!file) {
std::cerr << "Failed to open file.\n";
co_return;
}
while (true) {
auto chunk_opt = co_await AsyncRead(file, 1024);
if (!chunk_opt) break; // EOF
const auto &chunk = *chunk_opt;
std::cout.write(chunk.data(), file.gcount());
}
co_return;
}
int main() {
auto handle = read_file_async("sample.bin");
handle.resume(); // 开始执行
// 在此可做其他工作,协程会在后台异步完成
std::this_thread::sleep_for(std::chrono::seconds(2));
return 0;
}
代码说明
-
AsyncReadawait_ready()始终返回false,表示需要挂起。await_suspend()启动一个新线程模拟异步读取,并在读取完成后调用caller.resume()。await_resume()将读取到的数据返回给协程。
-
read_file_async- 通过
co_await AsyncRead挂起等待文件块,读取完成后继续。 co_return在文件结束后终止协程。
- 通过
-
主函数
- 调用
read_file_async得到协程句柄并立即resume(),随后主线程可以继续执行其他任务。
- 调用
四、总结
- C++20 原生协程为异步编程提供了更轻量、更易读的实现方式。
- 关键是理解协程的挂起/恢复、
promise_type、以及 Awaitable 的三个接口。 - 在实际项目中,可以将协程与 I/O 框架(如 Boost.Asio、libuv)结合,进一步提升性能。
通过上述原理解析与完整示例,相信你已经可以在自己的 C++ 项目中自由使用协程,实现更高效、可读性更好的异步代码。