C++17 在标准库中引入了协程(Coroutines),为异步编程和延迟计算提供了更直观、更高效的工具。协程的核心思想是让函数能够在执行过程中暂停,并在需要时恢复,保持本地状态而不必将整个函数堆栈压栈。本文将从协程的基本概念、关键类型、实现原理、常见使用场景以及一个完整的实战示例来系统阐述。
1. 协程的基本概念
协程是可以在多次调用间保持其执行状态的函数。与传统函数相比,协程可以在任意点 co_await、co_yield 或 co_return 暂停,然后在后续继续执行。其关键特性包括:
- 挂起点:
co_await、co_yield、co_return三种挂起点。 - 状态保持:协程内部的局部变量在挂起后仍保持其值。
- 控制流:协程返回一个 promise 对象,调用者可通过该对象获取结果或等待完成。
协程让异步逻辑可以像同步代码一样书写,显著降低复杂度。
2. 关键类型与语法
C++协程主要涉及以下类型:
| 类型 | 说明 |
|---|---|
std::suspend_always |
永远挂起,常用于测试或实现自定义调度器。 |
std::suspend_never |
从不挂起,常用在非挂起的场景。 |
std::coroutine_handle<> |
用于操作协程的句柄,能够 resume、destroy 等。 |
| `std::generator | |
| C++23 标准提供的生成器,类似yield` 的功能。 |
语法示例
#include <iostream>
#include <coroutine>
#include <exception>
struct my_task {
struct promise_type {
my_task get_return_object() {
return my_task{std::coroutine_handle <promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { std::terminate(); }
void return_void() {}
};
std::coroutine_handle <promise_type> handle;
explicit my_task(std::coroutine_handle <promise_type> h) : handle(h) {}
~my_task() { if (handle) handle.destroy(); }
void start() { if (handle) handle.resume(); }
};
my_task example() {
std::cout << "Step 1\n";
co_await std::suspend_always{};
std::cout << "Step 2\n";
co_return;
}
int main() {
auto t = example();
t.start(); // Step 1
t.start(); // Step 2
}
上述代码展示了一个最小的协程实现:协程在 co_await 处挂起,随后再次 resume 继续执行。
3. 协程实现原理
C++ 协程在编译时被转换为一个状态机。编译器会:
- 生成结构体:包含协程内部状态(例如
current_state、局部变量)。 - 生成
promise_type:实现协程的生命周期接口。 - 生成
resume方法:根据current_state跳转到相应位置。 - 挂起点:在
co_await、co_yield、co_return位置插入状态改变代码。
状态机转换表(简化):
| 当前状态 | 代码行 | 下一状态 |
|---|---|---|
| 0 | co_await std::suspend_always{} |
1 |
| 1 | std::cout << "Step 2" |
2 |
| 2 | co_return |
Finished |
4. 常见使用场景
| 场景 | 协程优势 |
|---|---|
| 异步 I/O | 通过 co_await 让 I/O 完成后再恢复,代码更像同步。 |
| 数据流 | 使用 `generator |
| ` 逐个生成值,内存占用低。 | |
| 游戏循环 | 每帧挂起/恢复状态,避免繁琐的状态机代码。 |
| 协程调度 | 在自定义调度器中挂起/恢复,支持优先级调度。 |
5. 真实案例:使用协程实现异步文件读取
下面演示如何使用 co_await 与标准库中的 std::future 搭配,实现一个简单的异步文件读取器。
#include <iostream>
#include <fstream>
#include <string>
#include <coroutine>
#include <future>
#include <chrono>
struct async_file_reader {
struct promise_type {
std::future<std::string> get_return_object() {
return std::async(std::launch::async, [this]() { return std::move(result); });
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { std::terminate(); }
void return_value(std::string&& str) { result = std::move(str); }
std::string result;
};
std::future<std::string> fut;
explicit async_file_reader(std::future<std::string> f) : fut(std::move(f)) {}
};
async_file_reader read_file_async(const std::string& path) {
std::ifstream file(path);
std::string content((std::istreambuf_iterator <char>(file)),
std::istreambuf_iterator <char>());
co_return std::move(content);
}
int main() {
auto reader = read_file_async("example.txt");
// 这里可以继续执行其他任务
std::cout << "File read in background.\n";
// 等待结果
std::string data = reader.fut.get();
std::cout << "File content:\n" << data << '\n';
}
async_file_reader封装了协程,返回一个std::future,允许在外部继续执行其他逻辑。read_file_async在后台读取文件,挂起期间不阻塞主线程。
6. 协程调度器:自定义事件循环
协程本身不包含调度逻辑,通常与事件循环结合使用。以下是一个简易的事件循环示例,支持多协程挂起/恢复。
#include <queue>
#include <functional>
#include <coroutine>
class EventLoop {
public:
using Task = std::coroutine_handle<>;
void push(Task t) { tasks.push(t); }
void run() {
while (!tasks.empty()) {
Task t = tasks.front(); tasks.pop();
if (!t.done()) t.resume();
else t.destroy();
}
}
private:
std::queue <Task> tasks;
};
EventLoop loop;
Task timer_task(std::chrono::milliseconds ms) {
std::cout << "Timer started\n";
co_await std::suspend_always{};
std::this_thread::sleep_for(ms);
std::cout << "Timer finished\n";
}
int main() {
loop.push(timer_task(1000));
loop.run();
}
此处 timer_task 在挂起点等待,事件循环恢复后执行后续代码。通过将挂起点与回调相结合,可实现复杂的协程调度。
7. 常见坑与最佳实践
- 忘记销毁协程句柄:未销毁会导致内存泄漏,最好使用 RAII 包装。
- 过度使用
co_await std::suspend_always{}:在生产环境中,应使用真正的异步操作或自定义调度器。 - 与异常结合:在
promise_type::unhandled_exception中捕获并处理,否则会直接terminate()。 - 性能开销:协程状态机会增加字节码,使用
std::suspend_always与std::suspend_never进行微调。 - 跨平台支持:C++17 协程已在大多数主流编译器实现,但仍需确认目标平台的实现细节。
8. 小结
- 协程让异步代码更易读、易维护。
- C++17 引入协程核心原语,C++20 对标准库进一步完善。
- 通过
co_await、co_yield、co_return挂起点实现状态机。 - 与
std::future、自定义事件循环配合,可构建高效的异步框架。 - 谨慎管理协程生命周期,避免资源泄漏。
掌握协程后,你将能够轻松实现高并发 I/O、流式数据处理以及复杂的游戏逻辑。继续深入阅读标准库文档,实践各种场景,相信你能在 C++ 编程中驾驭协程的强大力量。