在 C++20 中,协程(coroutines)被正式引入,彻底改变了异步编程的方式。它们让我们可以用顺序代码来描述异步流程,从而大幅提升代码的可读性与可维护性。本文将从协程的概念、核心语法、实现原理,到一个完整的实战案例,逐步带你走进协程的世界。
1. 协程到底是什么?
协程是一种能够暂停与恢复执行的函数(或生成器)。与普通函数不同,协程可以在执行过程中挂起自己,随后再恢复继续执行。它们是 轻量级 的线程化工具,内部不需要调度器、线程栈等复杂机制。
在 C++20 中,协程通过四个关键关键词实现:
| 关键词 | 作用 |
|---|---|
co_await |
等待一个异步操作完成 |
co_yield |
产生一个值(生成器) |
co_return |
返回协程结果 |
co_suspend |
手动挂起协程 |
协程的返回类型不是普通类型,而是 std::coroutine_handle 或者更常见的 std::future、std::generator 等适配器。
2. 基本语法
2.1 定义协程
#include <coroutine>
#include <iostream>
#include <string>
struct Awaitable {
std::string value;
Awaitable(std::string v) : value(std::move(v)) {}
bool await_ready() const noexcept { return false; } // 需要挂起
void await_suspend(std::coroutine_handle<> h) const noexcept {
std::cout << "Suspending: " << value << '\n';
}
std::string await_resume() const noexcept { return value; }
};
std::string asyncHello() {
std::string result = co_await Awaitable("Hello, coroutine!");
return result + " World!";
}
await_ready:判断是否需要挂起。若返回true,协程直接继续执行,否则挂起。await_suspend:挂起时的回调。通常会把协程句柄传给外部事件循环或线程池。await_resume:挂起结束后,返回值给调用者。
2.2 调用协程
int main() {
std::string res = asyncHello(); // 这行不会直接得到结果
// 实际上会在 await_suspend 里挂起,之后手动恢复
}
但在真实应用中,协程通常与 事件循环 或 异步运行时 配合使用。
3. 协程的实现原理
协程在编译器层面会被展开为一个 状态机。每一次 co_await、co_yield 或 co_return 对应状态机的一个分支。编译器会生成:
- Promise:承载协程的状态、返回值、异常。
- Suspend/Resume:实现挂起/恢复逻辑。
- Handle:用于外部控制协程生命周期。
重要点:协程本身不需要堆栈,它们使用 单一栈(通常是调用者栈)来保存局部变量;而状态机的状态则由编译器维护在堆上。
4. 一个完整实战:异步文件读取
以下示例演示如何使用协程实现一个简单的异步文件读取,并与 std::async 结合完成 I/O 线程池。
#include <coroutine>
#include <iostream>
#include <fstream>
#include <string>
#include <future>
#include <thread>
#include <chrono>
struct AsyncRead {
std::ifstream &ifs;
std::string buffer;
size_t size;
AsyncRead(std::ifstream &f, size_t sz) : ifs(f), size(sz) {}
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) const noexcept {
std::thread([h, this](){
buffer.resize(size);
ifs.read(buffer.data(), size);
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟异步延迟
h.resume();
}).detach();
}
std::string await_resume() const noexcept { return buffer; }
};
std::string asyncReadFile(std::ifstream &file, size_t sz) {
std::string data = co_await AsyncRead(file, sz);
return data;
}
int main() {
std::ifstream file("sample.txt", std::ios::binary);
if (!file) { std::cerr << "open failed\n"; return 1; }
std::string result = asyncReadFile(file, 1024);
std::cout << "Read data: " << result.substr(0, 100) << "...\n";
}
说明:
AsyncRead的await_suspend中创建了一个线程来执行真正的 I/O 操作,完成后通过h.resume()恢复协程。- 主程序等待协程完成后继续执行。
在生产环境中,你会用更完善的线程池、任务调度器来管理协程的挂起与恢复。
5. 协程 vs. 线程
| 特性 | 协程 | 线程 |
|---|---|---|
| 内存占用 | 轻量(几 KB) | 大量(几 MB) |
| 上下文切换 | 只在用户态 | 需要操作系统切换 |
| 并发数 | 数千到数百万 | 受限于系统资源 |
| 编程模型 | 线性、可读性强 | 需要锁、消息传递 |
| 适用场景 | I/O 密集、网络服务、游戏循环 | CPU 密集、硬件控制 |
6. 小结
- C++20 的协程提供了更自然、更可读的异步编程方式。
- 关键关键词
co_await、co_yield、co_return控制协程的挂起与恢复。 - 协程通过状态机实现,内部无需完整栈,降低资源消耗。
- 与事件循环、线程池结合,可构建高性能网络框架、游戏引擎、数据流处理。
进一步学习建议:阅读《C++协程实战》或使用
Boost.Coroutine2、cppcoro等库深入理解。
祝你在协程的世界里玩得开心,写出高效优雅的 C++ 代码!