在 C++20 中,协程(Coroutines)被正式引入,使得异步操作不再需要回调链或手动状态机。通过 co_await、co_yield 和 co_return,开发者可以以同步的思维写出真正异步、非阻塞的代码。本文从协程的基本原理开始,介绍常见的协程类型,给出完整的异步文件读取示例,并讨论与传统 std::future 的差异与适用场景。
1. 协程的基本概念
协程是一种可挂起(suspend)和恢复(resume)的函数。与普通函数不同,协程在执行过程中可以在某个点暂停,随后在需要时继续执行,而不必在调用点等待完成。C++ 的协程实现依赖于以下三个关键关键字:
co_await:在协程中等待一个 awaitable 对象,挂起当前协程直到该对象完成。co_yield:产生一个值,并挂起协程,等待下次获取。co_return:结束协程并返回最终值。
协程的底层实现利用 协程句柄(std::coroutine_handle)和 协程 promise(promise_type)来管理状态。编译器会为每个协程生成一个隐藏的状态机,负责保存局部变量、控制流以及挂起点。
2. 协程的三种主要形式
| 类型 | 关键字 | 典型用法 |
|---|---|---|
| 异步函数 | co_await |
`awaitable |
| foo();` | ||
| 生成器 | co_yield |
`generator |
| seq();` | ||
| 协程返回值 | co_return |
`Task |
| async_task();` |
- 异步函数:返回一个 awaitable,调用方使用
co_await等待结果。适合网络 IO、磁盘 IO 等 I/O 密集型任务。 - 生成器:类似 C# 的
yield return,一次返回一个值,常用于遍历序列。返回类型通常是 `generator ` 或自定义类型。 - 协程返回值:类似于
std::future,但更轻量且不需要线程池。适用于非阻塞计算。
3. 经典示例:异步读取文件
下面给出一个完整的异步文件读取实现,演示如何将标准文件 I/O 包装成 awaitable,并在主协程中使用 co_await。
#include <coroutine>
#include <iostream>
#include <vector>
#include <fstream>
#include <string>
#include <system_error>
#include <thread>
#include <chrono>
#include <optional>
// 1. Awaitable 结构体
struct AsyncRead {
std::string filename;
std::vector <char> buffer;
std::optional<std::error_code> ec; // 读取错误
struct promise_type {
AsyncRead get_return_object() { return {nullptr, std::vector <char>(), std::nullopt}; }
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
using handle_type = std::coroutine_handle <promise_type>;
// 让协程可以被 await
struct awaiter {
AsyncRead &ar;
bool await_ready() noexcept { return false; }
void await_suspend(handle_type h) noexcept {
std::thread([ar = ar, h]() mutable {
try {
std::ifstream ifs(ar.filename, std::ios::binary);
if (!ifs) throw std::system_error(errno, std::generic_category(), "open file");
ar.buffer.assign((std::istreambuf_iterator <char>(ifs)),
std::istreambuf_iterator <char>());
} catch (...) {
ar.ec = std::make_optional(std::error_code(errno, std::generic_category()));
}
h.resume(); // 读取完成后恢复协程
}).detach();
}
std::vector <char> await_resume() noexcept {
if (ar.ec) throw std::system_error(*ar.ec);
return std::move(ar.buffer);
}
};
awaiter operator co_await() { return { *this }; }
};
// 2. 异步读取函数
AsyncRead read_file_async(const std::string &name) {
co_return std::move(name);
}
// 3. 主协程
struct Run {
struct promise_type {
Run get_return_object() { return {}; }
std::suspend_never initial_suspend() noexcept { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
};
Run main_coroutine() {
try {
std::vector <char> data = co_await read_file_async("example.txt");
std::cout << "文件大小: " << data.size() << " 字节\n";
} catch (const std::system_error &e) {
std::cerr << "读取失败: " << e.what() << '\n';
}
}
int main() {
main_coroutine(); // 直接执行主协程
std::this_thread::sleep_for(std::chrono::seconds(1)); // 等待异步读取完成
return 0;
}
说明:
AsyncRead是一个 awaitable,内部使用std::thread异步读取文件。实际生产环境建议使用专门的异步 I/O 库(如 libuv、asio)来避免创建大量线程。co_await read_file_async让主协程挂起,等到AsyncRead完成后恢复。main_coroutine通过co_return结束,示例中我们把协程作为普通函数直接调用。
4. 与 std::future 的比较
| 特点 | std::future |
C++20 协程 |
|---|---|---|
| 执行模型 | 通常绑定到线程池或同步任务 | 线程无关、可挂起 |
| 错误传播 | 通过异常或 future::get() |
通过 await_resume() 抛出 |
| 性能 | 需要上下文切换、对象拷贝 | 仅在挂起点产生状态机,开销极小 |
| 使用场景 | 简单异步结果获取 | 复杂异步流程、需要多次挂起、生成器等 |
协程在处理大量 I/O 时可以显著降低资源消耗,尤其在需要高并发时,它们的“非阻塞挂起”特性使得单线程事件循环也能并行执行。
5. 实践建议
- 不要滥用线程:协程本身不产生线程,只是编译器生成状态机。真正的异步 I/O 必须依赖 OS 提供的事件驱动或第三方库。
- 使用
co_await时注意对象生命周期:协程句柄会在协程结束时销毁,确保 awaitable 的内部资源不会悬挂。 - 与现有
std::future混合:可以将协程包装成std::future,或在协程里co_await std::async。 - 学习
generator:用于生成大数据序列时,比一次性生成整份数据更节省内存。
6. 结语
C++20 的协程为语言注入了一种全新的异步表达方式,让复杂的异步逻辑变得像同步代码一样直观。掌握协程后,你可以:
- 用
co_await编写事件驱动网络服务器; - 用
co_yield创建高效的流式处理管道; - 用
co_return简化异步任务的返回。
未来的标准可能会进一步扩展协程的功能(如 await_transform、task 类型)。现在就去试试上述示例,感受一下“协程编程”带来的新风貌吧!