C++20 引入了协程(coroutines)这一强大的语言特性,极大地简化了异步编程、生成器以及惰性求值等场景的实现。下面从概念、实现细节以及常见使用案例几个角度,详细剖析协程的工作原理。
1. 协程概念回顾
- 协程:在运行过程中能够挂起(suspend)和恢复(resume)的函数。它们的执行状态被保留,能够在不同时间点间断执行。
- 关键字:
co_await、co_yield、co_return,以及协程返回类型std::coroutine_handle。 - 目标:把异步或惰性计算的流程拆分成若干个挂起点,让调用者可以像同步代码一样书写。
2. 代码结构与关键字
#include <coroutine>
#include <iostream>
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};
Task example() {
std::cout << "Start\n";
co_await std::suspend_always{}; // 挂起点 1
std::cout << "Resume\n";
co_return; // 结束
}
- promise_type:每个协程都有对应的 promise 对象,用来管理协程的生命周期和返回值。
- initial_suspend / final_suspend:分别决定协程在开始和结束时是否挂起。
- co_await / co_yield / co_return:在协程内部的挂起点。
3. 协程底层实现(简化版)
-
生成器栈
C++ 编译器在编译协程时会把函数体拆分成若干个基本块,并在栈上为每个挂起点保存局部变量的快照(称为“状态机”)。 -
状态机
编译器将协程视作一个有限状态机(FSM)。每个挂起点对应一个状态,执行到挂起点时会把当前状态保存在协程句柄中,随后返回控制权。 -
协程句柄
` 保存了协程状态、返回地址和 promise 对象。通过 `handle.resume()` 可以恢复协程。
`std::coroutine_handle -
内存管理
协程对象本身不持有堆内存,所有局部变量都保存在堆上(由协程句柄管理)。当协程完成时,final_suspend的suspend_always触发后,资源被释放。
4. 常见使用场景
| 场景 | 典型实现 | 优点 |
|---|---|---|
| 异步 I/O | co_await asyncRead() |
代码可读性高,回调链消失 |
| 生成器 | co_yield value |
惰性迭代,内存占用小 |
| 管道/流 | co_yield 组合 |
直观的流水线处理 |
| 协程化线程 | co_spawn + awaitable |
更细粒度调度 |
5. 一个完整的异步文件读取示例
#include <coroutine>
#include <iostream>
#include <fstream>
#include <string>
#include <future>
struct AwaitableRead {
std::ifstream& stream;
char buffer[1024];
std::size_t nread;
AwaitableRead(std::ifstream& s) : stream(s) {}
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) noexcept {
// 简单模拟异步,实际应使用事件驱动或线程池
std::async(std::launch::async, [this, h]() mutable {
stream.read(buffer, sizeof(buffer));
nread = stream.gcount();
h.resume();
});
}
std::size_t await_resume() const noexcept { return nread; }
};
struct AsyncFileReader {
std::ifstream file;
AsyncFileReader(const std::string& path) : file(path, std::ios::binary) {}
std::future<std::size_t> readChunk() {
co_return co_await AwaitableRead(file);
}
};
int main() {
AsyncFileReader reader("example.bin");
auto future = reader.readChunk();
std::size_t bytes = future.get();
std::cout << "Read " << bytes << " bytes.\n";
}
- 通过自定义
AwaitableRead,我们把同步读取包装成协程可等待对象,内部使用std::async模拟异步行为。 - 调用方使用
co_await与std::future搭配,保持了同步语义。
6. 性能与陷阱
- 开销:协程的状态机、堆分配和上下文切换会带来一定成本。对极小粒度操作建议使用回调或同步方式。
- 异常安全:如果协程中抛出异常,
promise_type::unhandled_exception会被调用,需要自行决定是否将异常抛出给外层。 - 内存泄漏:协程句柄忘记销毁或
final_suspend未返回suspend_always可能导致资源泄漏。
7. 结语
C++20 协程为语言层面提供了强大的异步控制流能力。掌握其基本概念、编译器生成的状态机以及常见使用模式,可以让你在高性能计算、网络编程和数据流处理等领域书写更简洁、更易维护的代码。下一步可以尝试结合 std::experimental::generator 或第三方库(如 asio)深入学习协程的实际应用。