一、引言
自 C++20 起,标准库正式加入协程(coroutine)支持,标志着 C++ 语言进入了异步编程的新时代。协程本质上是可以被挂起与恢复的函数,它让“写同步、运行异步”的编程模式变得简单自然。本文从基本概念出发,结合完整的 std::async、std::future 与自定义 awaitable 对象,演示如何在 C++ 中使用协程完成高效的异步 I/O,并给出常见问题与优化技巧。
二、协程的核心组成
- co_await:挂起当前协程并等待 awaitable 对象完成。
- co_yield:在生成器模式中产生一个值并挂起协程。
- co_return:结束协程并返回一个值。
- promise_type:协程内部实现的桥梁,管理协程状态与返回值。
- awaiter:实现
await_ready()、await_suspend()与await_resume()的对象。
三、实现一个异步文件读取协程
下面给出一个完整的例子,演示如何使用 std::filesystem 与 std::async 结合协程实现异步文件读取。
#include <coroutine>
#include <future>
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <filesystem>
// awaitable 对象:包装 std::future
template<typename T>
struct AsyncAwaitable {
std::future <T> fut;
AsyncAwaitable(std::future <T> f) : fut(std::move(f)) {}
bool await_ready() const noexcept { return fut.wait_for(std::chrono::seconds(0)) == std::future_status::ready; }
void await_suspend(std::coroutine_handle<> h) {
std::thread([this, h]() mutable {
fut.wait(); // 阻塞等待
h.resume(); // 继续协程
}).detach();
}
T await_resume() { return fut.get(); }
};
// 协程函数:异步读取文件
std::future<std::string> async_read_file(const std::filesystem::path& path) {
return std::async(std::launch::async, [&path]() {
std::ifstream file(path, std::ios::binary);
if (!file) return std::string("文件打开失败");
std::string content((std::istreambuf_iterator <char>(file)),
std::istreambuf_iterator <char>());
return content;
});
}
// 使用协程的异步主函数
struct AsyncMain {
struct promise_type {
AsyncMain get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void unhandled_exception() { std::terminate(); }
void return_void() {}
};
};
AsyncMain async_main() {
std::filesystem::path p = "example.txt";
std::string data = co_await AsyncAwaitable<std::string>(async_read_file(p));
std::cout << "文件内容长度: " << data.size() << " 字节\n";
std::cout << "内容前 100 字节:\n" << data.substr(0, 100) << "\n";
}
关键点说明
- AsyncAwaitable:包装
std::future并实现协程挂起与恢复。 - async_read_file:使用
std::async创建后台线程读取文件。 - async_main:示例协程,展示如何
co_await异步 I/O 并处理结果。
四、协程与传统线程池的对比
| 维度 | 传统线程池 | C++ 协程 |
|---|---|---|
| 资源占用 | 每个任务需要完整线程(大约 1MB 堆栈) | 协程仅占用几 KB 的栈帧,挂起后不占用线程 |
| 调度模型 | 线程调度器决定 | 协程调度交给程序,易于实现自定义事件循环 |
| 错误传播 | 通过异常或回调 | 通过 promise_type::unhandled_exception 统一异常传播 |
| 可读性 | 需要回调或 std::future 链 |
co_await 让异步代码像同步一样线性 |
五、常见陷阱与解决方案
-
未使用
std::launch::asyncstd::async默认行为为std::launch::deferred,可能导致同步阻塞。- 解决:显式指定
std::launch::async。
-
未正确管理协程生命周期
- 协程句柄在
await_suspend结束后若未resume(),协程会悬挂。 - 解决:在 awaiter 的
await_suspend中手动h.resume()。
- 协程句柄在
-
异常传播失效
- 如果在 awaiter 的
await_resume()抛异常,需在promise_type::unhandled_exception()中处理。 - 解决:实现自定义异常处理逻辑,或在调用方使用
try/catch包裹。
- 如果在 awaiter 的
-
过度使用协程导致堆栈溢出
- 递归协程如果深度过大仍会耗尽栈。
- 解决:将深层递归改为循环或使用尾递归优化。
六、进阶主题:自定义事件循环
在大规模网络服务器中,通常需要一个事件循环(Event Loop)来驱动协程。示例伪代码:
struct Scheduler {
std::deque<std::coroutine_handle<>> ready;
void schedule(std::coroutine_handle<> h) { ready.push_back(h); }
void run() {
while (!ready.empty()) {
auto h = ready.front();
ready.pop_front();
h.resume();
}
}
};
协程在 await_suspend 时调用 Scheduler::schedule(h) 将自己重新加入队列,从而实现非阻塞事件驱动。
七、结语
C++20 的协程为异步编程提供了与同步代码同等的可读性与可维护性。通过 awaitable 对象包装标准库中的异步设施(如 std::future、std::async),或者自定义事件驱动,开发者可以轻松实现高性能、低资源占用的 I/O 任务。随着社区生态的发展,越来越多的库(如 Boost.Asio、libuv 等)已经开始支持协程,使得 C++ 在高性能网络、游戏开发与系统编程领域拥有更广阔的前景。