协程(Coroutines)是C++20引入的一个强大特性,它让异步编程变得像同步编程一样自然和简洁。下面我们从协程的基本概念开始,逐步深入到实际使用场景,帮助你快速掌握并应用到项目中。
1. 协程是什么?
协程是一种比线程更轻量级的“伪线程”概念。它允许函数在执行过程中挂起(co_await)、恢复(co_yield)或终止,而不需要手动管理线程或状态机。协程内部维护一个状态机,用来记录函数的暂停点和局部变量的值。
2. 协程的核心关键词
| 关键词 | 含义 | 典型用法 |
|---|---|---|
co_await |
挂起协程,等待一个可等待对象完成 | co_await async_operation(); |
co_yield |
生成一个值给调用者,暂停协程 | co_yield 42; |
co_return |
结束协程并返回一个值 | co_return 0; |
3. 一个简单的协程示例
#include <iostream>
#include <coroutine>
#include <thread>
#include <chrono>
// 1. 定义一个可等待的类型
struct SleepAwaitable {
std::chrono::milliseconds duration;
bool await_ready() const noexcept { return duration.count() == 0; }
void await_suspend(std::coroutine_handle<> h) const {
std::thread([h, dur = duration]() {
std::this_thread::sleep_for(dur);
h.resume(); // 唤醒协程
}).detach();
}
void await_resume() const noexcept {}
};
SleepAwaitable sleep_for(std::chrono::milliseconds ms) {
return SleepAwaitable{ms};
}
// 2. 协程返回类型
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
};
// 3. 协程函数
Task example_coroutine() {
std::cout << "Start\n";
co_await sleep_for(std::chrono::seconds(1));
std::cout << "Middle after 1s\n";
co_await sleep_for(std::chrono::seconds(2));
std::cout << "End after 2s\n";
}
int main() {
example_coroutine(); // 协程立即执行,挂起后继续
std::this_thread::sleep_for(std::chrono::seconds(4)); // 主线程等待
}
运行结果:
Start
Middle after 1s
End after 2s
这个例子展示了如何用 co_await 挂起协程,等待一个自定义的 SleepAwaitable 完成。
4. 协程与 std::generator
C++20 标准库中提供了 std::generator,专门用于生成一系列值。它结合 co_yield 的语义,像 Python 的生成器一样简单。
#include <iostream>
#include <generator>
std::generator <int> range(int start, int end) {
for (int i = start; i <= end; ++i)
co_yield i;
}
int main() {
for (auto v : range(1, 5))
std::cout << v << ' ';
}
输出:
1 2 3 4 5
5. 协程与异步 I/O
协程是实现异步 I/O 的理想选择。典型框架(如 Boost.Asio、cpprestsdk、Pika)都在内部使用协程来隐藏事件循环的细节。下面给出一个伪代码示例,演示如何用协程包装异步文件读取。
struct AsyncRead {
std::filesystem::path file;
struct promise_type { /* 同上 */ };
// ...
};
AsyncRead async_read_file(const std::string& path) {
// 这里使用操作系统提供的异步 API
co_await // ...
// 读取完成后返回数据
co_return std::vector <char>{...};
}
Task main_task() {
auto data = co_await async_read_file("data.txt");
std::cout << "Read " << data.size() << " bytes\n";
}
6. 性能与坑
- 栈占用:协程的栈在编译时展开为状态机,局部变量会保存在堆栈上,但并不是线程栈,大小取决于你在协程里声明的变量。避免在协程里使用过大的数组。
- 异常处理:协程内部异常会调用
unhandled_exception。你可以在promise_type中提供自定义异常捕获逻辑。 - 与线程混用:协程本身不涉及线程切换,但如果你在协程里使用
std::thread或者阻塞操作,仍然会产生线程上下文切换。建议把耗时操作封装成co_await的异步接口。
7. 小结
- C++20 的协程让异步代码更加直观,几乎没有语法负担。
co_await用于等待异步操作;co_yield用于生成序列;co_return用于返回结果。- 通过自定义 awaitable,可以将任何异步 API(网络、文件、定时器)变成协程友好。
std::generator为生成器提供了标准实现。
掌握协程后,你可以轻松构建高性能的网络服务器、游戏引擎异步任务系统,甚至实现自己的协程框架。祝你玩得开心,编码愉快!