协程(Coroutine)是 C++20 引入的一项强大特性,允许函数在执行过程中暂停并在以后恢复,从而实现异步编程、生成器以及更直观的流控制。相比传统的回调或 Future,协程在语义上更接近同步代码,降低了复杂度。本文将从概念入手,介绍协程的基本构造、典型使用场景、关键 API 以及常见坑,配合完整示例帮助你快速上手。
1. 协程基本概念
| 关键词 | 解释 |
|---|---|
co_await |
在协程内部等待一个 awaitable 对象(如 std::future、自定义 Awaitable 等) |
co_yield |
生成一个值,类似生成器中的 yield |
co_return |
结束协程并返回结果 |
co_await std::suspend_always / std::suspend_never |
明确指定协程何时挂起或不挂起 |
协程并非单独线程,而是一种轻量级的状态机。编译器会将带有协程关键字的函数展开为一个状态机类,内部维护悬挂点和返回值。
2. 标准协程接口
2.1 std::suspend_always 与 std::suspend_never
struct suspend_always {
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<>) const noexcept {}
void await_resume() const noexcept {}
};
struct suspend_never {
bool await_ready() const noexcept { return true; }
void await_suspend(std::coroutine_handle<>) const noexcept {}
void await_resume() const noexcept {}
};
suspend_always:协程始终挂起,等待外部显式 resume。suspend_never:协程不挂起,直接执行完毕。
2.2 std::coroutine_handle
template<class Promise>
struct coroutine_handle {
static coroutine_handle from_promise(Promise& promise);
void resume(); // 恢复执行
bool done() const; // 是否已结束
void destroy(); // 释放资源
};
开发者通常不直接使用 coroutine_handle,除非需要自定义协程调度器。
3. 实际案例:异步文件读取
下面演示如何用协程实现异步读取文件,读取完毕后返回字符串。
#include <coroutine>
#include <string>
#include <iostream>
#include <fstream>
#include <filesystem>
struct AsyncReadResult {
std::string content;
bool success = false;
};
struct AsyncRead {
struct promise_type {
AsyncReadResult result;
std::coroutine_handle <promise_type> next{};
AsyncRead get_return_object() {
return AsyncRead{std::coroutine_handle <promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept {
// 这里可以让协程自动完成后执行下一步
if (next) next.resume();
return {};
}
void return_value(AsyncReadResult val) { result = val; }
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle <promise_type> handle;
AsyncRead(std::coroutine_handle <promise_type> h) : handle(h) {}
AsyncRead(const AsyncRead&) = delete;
AsyncRead& operator=(const AsyncRead&) = delete;
~AsyncRead() {
if (handle) handle.destroy();
}
void resume() { if (!handle.done()) handle.resume(); }
};
AsyncRead read_file_async(const std::string& path) {
co_await std::suspend_always{}; // 模拟异步挂起
AsyncReadResult res;
try {
std::ifstream in(path, std::ios::binary);
if (!in) throw std::runtime_error("打开文件失败");
std::string data((std::istreambuf_iterator <char>(in)),
std::istreambuf_iterator <char>());
res.content = std::move(data);
res.success = true;
} catch (...) {
res.success = false;
}
co_return res;
}
int main() {
auto task = read_file_async("example.txt");
// 手动调度协程
task.resume(); // 第一次 resume 会进入协程主体
// 由于我们在协程中使用了 suspend_always,第二次 resume 继续执行
task.resume();
auto& result = task.handle.promise().result;
if (result.success) {
std::cout << "读取内容:" << result.content.substr(0, 100) << "...\n";
} else {
std::cout << "读取失败\n";
}
}
说明
AsyncRead封装了协程句柄与 promise,简化使用。- 通过
co_await std::suspend_always{}模拟异步挂起,实际项目可替换为真正的 I/O 完成事件。 final_suspend用于在协程结束后自动恢复下一步操作,演示协程链式调用的便利。
4. 协程的常见陷阱
| 陷阱 | 解决办法 |
|---|---|
忘记 co_return |
必须在协程尾部使用 co_return 或抛异常,避免隐式返回导致未定义行为。 |
| promise 对象被提前销毁 | 保证协程句柄存活至协程结束,避免在外部捕获 std::coroutine_handle 时误删。 |
| 异常传播 | 在 promise_type 中实现 unhandled_exception,将异常包装或记录,避免程序崩溃。 |
| 多线程协程 | 协程本身并非线程安全,若在多线程中共享协程对象,需要使用互斥或专用调度器。 |
| 资源泄漏 | 始终在 final_suspend 或 ~AsyncRead 中释放句柄和 promise;不要忘记调用 destroy()。 |
5. 协程与传统异步框架对比
| 特性 | 协程 | std::future / std::async |
|---|---|---|
| 代码风格 | 同步写法,易读 | 回调或链式 Future,代码可读性下降 |
| 性能 | 轻量级,堆栈共享 | 线程/线程池开销 |
| 错误处理 | 直接抛异常 | 需要检查状态 |
| 调度 | 可自定义调度器 | 受线程池限制 |
如果项目中需要频繁进行 I/O、网络请求或需要实现生成器,协程无疑是更优选。
6. 小结
- C++20 协程通过
co_await / co_yield / co_return简化异步编程。 - 标准库提供
std::suspend_always / suspend_never、coroutine_handle等工具。 - 示例演示了异步文件读取的完整实现。
- 注意异常传播、资源释放和线程安全,避免常见坑。
掌握协程后,你将能够编写更简洁、更高效的异步代码,提升项目的可维护性与性能。祝编码愉快!