在 C++20 标准发布后,协程(coroutines)成为了语言中一个极具潜力的特性,它可以让我们以更直观、更高效的方式编写异步代码。本文将从协程的基本概念、实现机制、使用场景以及常见坑点几个角度,帮助你快速上手并掌握 C++ 协程的核心技巧。
1. 协程的基本概念
协程是“可挂起”与“可恢复”的函数。与传统函数不同,协程在执行过程中可以暂停(挂起),保存当前状态,然后在需要时恢复执行。C++20 的协程语法主要由以下关键词组成:
co_await:挂起协程,等待一个 awaitable 对象完成。co_yield:暂停当前协程,将一个值返回给调用者,等待下一个请求。co_return:结束协程,返回最终结果。
协程本身不拥有自己的栈;它通过“协程框架”来管理状态,使用 promise 对象来保存返回值、异常以及挂起点。
2. 协程的实现原理
当编译器遇到 co_await、co_yield、co_return 时,会把协程拆分成若干“块”。每个块代表一个挂起点。编译器在生成的状态机中使用 switch 或 jump 表实现挂起与恢复。
关键步骤:
- 生成 Promise 对象:协程入口函数返回一个 `std::coroutine_handle `,Promise 用来存储协程的返回值和异常。
- 调用
initial_suspend:决定协程是否立即开始或先挂起。 - 执行主体:在
try/catch结构里执行代码,遇到挂起点调用相应await_suspend。 - 完成后:调用
final_suspend,若返回true,协程立即销毁;若返回false,则留给外部决定何时销毁。
了解这一流程可以帮助你在调试时判断协程状态,定位性能瓶颈。
3. 实际使用案例
3.1 异步 I/O 读取文件
#include <coroutine>
#include <fstream>
#include <string>
#include <iostream>
struct FileReadResult {
std::string content;
};
struct FileReadAwaitable {
std::string path;
FileReadResult result;
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) {
std::thread([=]() mutable {
std::ifstream in(path);
std::string data((std::istreambuf_iterator <char>(in)),
std::istreambuf_iterator <char>());
result.content = std::move(data);
h.resume(); // 恢复协程
}).detach();
}
FileReadResult await_resume() { return std::move(result); }
};
FileReadAwaitable readFileAsync(const std::string& path) {
return FileReadAwaitable{path};
}
std::future <FileReadResult> readFile(std::string path) {
co_return co_await readFileAsync(std::move(path));
}
int main() {
auto future = readFile("example.txt");
auto result = future.get();
std::cout << "File content: " << result.content << '\n';
}
这个例子展示了如何将普通 I/O 操作包装成 awaitable,并通过协程让主线程非阻塞地等待文件读取完成。
3.2 生成斐波那契序列
#include <coroutine>
#include <iostream>
struct Fibonacci {
struct promise_type {
uint64_t value = 0;
uint64_t next = 1;
Fibonacci get_return_object() { return { std::coroutine_handle <promise_type>::from_promise(*this) }; }
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle <promise_type> handle;
Fibonacci(std::coroutine_handle <promise_type> h) : handle(h) {}
~Fibonacci() { if (handle) handle.destroy(); }
bool next(uint64_t& out) {
if (!handle.done()) {
out = handle.promise().value;
handle.resume();
return true;
}
return false;
}
};
Fibonacci fib_sequence() {
uint64_t a = 0, b = 1;
co_yield a;
co_yield b;
while (true) {
uint64_t c = a + b;
a = b;
b = c;
co_yield b;
}
}
int main() {
auto fib = fib_sequence();
uint64_t val;
for (int i = 0; i < 10 && fib.next(val); ++i)
std::cout << val << ' ';
}
这里使用 co_yield 实现了一个无限生成器,示例演示了协程如何与迭代器模式结合。
4. 常见坑点与优化
| 位置 | 说明 | 解决方案 |
|---|---|---|
await_ready |
未正确返回 true 时协程始终挂起 |
对同步操作返回 true |
promise_type 的析构 |
未释放资源导致内存泄漏 | 在 final_suspend 中返回 std::suspend_never 或手动销毁 |
| 阻塞 I/O | 在协程里直接调用阻塞函数 | 用 co_await 包装异步 API,或使用多线程 |
| 线程安全 | 协程 handle 不是线程安全 | 确保协程对象在单线程内使用,或使用同步机制 |
5. 未来展望
- 协程池:管理大量协程实例,避免频繁分配栈。
- 协程与网络框架:如
cppcoro、libuv结合使用,实现高性能网络服务。 - 协程的并行:配合
std::execution和std::transform_reduce,实现并行协程化计算。
结语
C++ 协程提供了一个强大的工具,让异步代码像同步代码一样简洁。只要掌握好基本语法与状态机实现细节,便能在高性能项目中大幅提升可读性与维护性。希望本文能帮助你快速踏入协程的世界,写出既优雅又高效的 C++ 程序。