在 C++20 标准中,协程(Coroutines)被正式纳入语言规范,为异步编程提供了更直观、性能更优的解决方案。与传统的回调、线程或手动状态机相比,协程让代码更接近同步写法,同时保持非阻塞和高效。本文将从协程的基本概念、关键语法到实战案例,逐步展开讲解。
一、协程的核心概念
-
暂停与恢复
协程通过co_await、co_yield、co_return关键字在执行过程中“挂起”与“恢复”,使得函数可以在多个点暂停执行,随后从上一次挂起的位置继续。 -
生成器(Generator)与任务(Task)
- Generator:使用
co_yield产生一系列值,类似于 Python 的生成器。 - Task:使用
co_return返回最终结果,类似于 Future/Promise。
- Generator:使用
-
协程的句柄(Coroutine Handle)
每个协程都有一个句柄std::coroutine_handle,它可用来检查协程是否已完成、主动恢复或销毁协程。
二、关键语法与实现细节
| 关键字 | 作用 | 典型示例 |
|---|---|---|
co_await |
等待一个 awaitable 对象,协程挂起直到其完成 | int value = co_await asyncRead(); |
co_yield |
生成一个值,暂停协程,让调用者获取该值 | co_yield i; |
co_return |
返回最终值,结束协程 | co_return result; |
co_await 前的 co_ 前缀 |
标识协程操作,编译器会生成相应的 state machine | co_await std::suspend_always{}; |
Awaitable 类型
任何具备以下成员的类型都可以作为 awaitable:
struct Awaitable {
bool await_ready() const noexcept;
void await_suspend(std::coroutine_handle<> h) noexcept;
T await_resume() noexcept;
};
await_ready():判断是否需要挂起。await_suspend():挂起协程,传入当前句柄。await_resume():协程恢复时返回的值。
三、实战示例:异步文件读取
下面给出一个完整的协程实现,用来异步读取文件内容,并返回字符串。示例使用了 std::filesystem 和 std::future 作为后端异步机制。
#include <coroutine>
#include <future>
#include <fstream>
#include <string>
#include <iostream>
struct AsyncReadResult {
std::string data;
bool success;
};
struct AsyncReadAwaitable {
std::string filename;
std::promise <AsyncReadResult> promise;
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) {
std::thread([this, h]() {
std::ifstream in(filename, std::ios::binary);
AsyncReadResult res;
if (in) {
res.data.assign((std::istreambuf_iterator <char>(in)),
std::istreambuf_iterator <char>());
res.success = true;
} else {
res.success = false;
}
promise.set_value(res);
h.resume(); // 这里不需要显式恢复,因为我们使用了 asyncFuture 的机制
}).detach();
}
AsyncReadResult await_resume() { return promise.get_future().get(); }
};
struct AsyncReadTask {
struct promise_type {
std::coroutine_handle <promise_type> get_return_object() {
return std::coroutine_handle <promise_type>::from_promise(*this);
}
std::suspend_never initial_suspend() noexcept { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void unhandled_exception() { std::terminate(); }
void return_value(AsyncReadResult value) { result = value; }
AsyncReadResult result;
};
std::coroutine_handle <promise_type> handle;
AsyncReadResult result;
AsyncReadTask(std::coroutine_handle <promise_type> h) : handle(h), result(h.promise().result) {}
~AsyncReadTask() { if (handle) handle.destroy(); }
};
AsyncReadTask asyncReadFile(const std::string& filename) {
AsyncReadResult res = co_await AsyncReadAwaitable{filename};
co_return res;
}
int main() {
auto task = asyncReadFile("example.txt");
if (task.result.success) {
std::cout << "文件内容:" << task.result.data << std::endl;
} else {
std::cout << "读取失败" << std::endl;
}
return 0;
}
说明
AsyncReadAwaitable:包装文件读取逻辑,内部用std::thread异步执行。asyncReadFile:使用协程实现的异步读取函数,返回AsyncReadTask。AsyncReadTask:协程句柄包装器,提供result成员供调用者直接访问。
四、性能与注意事项
-
协程本身无开销
协程在挂起/恢复时不涉及栈复制或线程切换,只是更新内部状态机。真正的开销来自于 awaitable 对象所做的异步操作。 -
不要滥用
过度使用协程会导致句柄数目暴增,尤其在高并发网络服务器中,需要对协程池或调度器做细粒度管理。 -
异常安全
协程在异常抛出时会走unhandled_exception(),默认调用std::terminate。如果需要捕获异常,可在promise_type::return_void或return_value里进行处理。
五、进一步阅读
- 《C++20 标准草案》相关章节
- 《Effective Modern C++》中关于协程的讨论
- 官方实现库如
cppcoro、asio::awaitable等
通过上述介绍,你应该能对 C++20 协程有一个系统的了解,并在实际项目中快速尝试异步编程。协程将成为 C++ 未来异步开发的核心工具,值得每位 C++ 开发者深入掌握。