协程(coroutine)是 C++20 的新特性,提供了一种更简洁、更高效的异步编程模型。与传统的回调或线程相比,协程可以在单线程内实现多任务切换,避免了大量上下文切换开销。下面从概念、核心类、使用方式以及常见问题几个方面,帮助你快速掌握协程的基本用法。
1. 协程的核心概念
| 术语 | 说明 |
|---|---|
| 协程函数 | 带有 co_await、co_yield 或 co_return 的函数 |
| 挂起点 | co_await, co_yield, co_return 的位置,协程会在此暂停 |
| 协程句柄 | `std::coroutine_handle |
| `,用于控制协程的生命周期 | |
| 协程承诺 | promise_type,协程内部的状态管理对象 |
| 状态机 | 编译器把协程函数自动转化为状态机 |
2. 基本协程例子
#include <coroutine>
#include <iostream>
#include <string_view>
struct Generator {
struct promise_type {
std::string_view current_value;
std::string_view await_transform(std::string_view value) { return value; }
Generator get_return_object() { return Generator{ std::coroutine_handle <promise_type>::from_promise(*this) }; }
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle <promise_type> coro;
explicit Generator(std::coroutine_handle <promise_type> h) : coro(h) {}
~Generator() { if (coro) coro.destroy(); }
bool next() {
coro.resume();
return !coro.done();
}
std::string_view value() const { return coro.promise().current_value; }
};
Generator countdown(int start) {
for (int i = start; i >= 0; --i) {
co_yield std::to_string(i);
}
}
int main() {
auto gen = countdown(5);
while (gen.next()) {
std::cout << "Value: " << gen.value() << '\n';
}
}
说明:
co_yield会把当前值保存到承诺对象中,然后挂起协程。coro.resume()重新激活协程,执行到下一个挂起点。std::coroutine_handle用于控制协程的开始、暂停、销毁。
3. co_await 与 awaitable
co_await 主要用来等待异步操作完成。要使一个类型可被 co_await,需要满足以下接口:
struct awaitable {
bool await_ready(); // 立即完成则返回 true
void await_suspend(std::coroutine_handle<> h); // 需要挂起
void await_resume(); // 恢复后返回的值
};
示例:等待一个异步 I/O 结果。
#include <future>
#include <chrono>
struct AsyncWait {
std::future <void> fut;
AsyncWait() : fut(std::async(std::launch::async, []{
std::this_thread::sleep_for(std::chrono::seconds(1));
})) {}
bool await_ready() { return fut.wait_for(std::chrono::seconds(0)) == std::future_status::ready; }
void await_suspend(std::coroutine_handle<> h) { std::thread([=]{
fut.get();
h.resume();
}).detach(); }
void await_resume() {}
};
Generator example() {
co_await AsyncWait{};
co_yield "After 1 second";
}
4. 协程的内存与生命周期
- 堆 vs 栈:编译器会把协程函数的局部变量在协程对象中放到堆(由
promise_type管理),保证挂起后状态保留。 - 析构:如果协程没有
final_suspend结束,就会导致资源泄露。通常使用std::suspend_always或std::suspend_never。 - 异常:在协程内部抛出异常会被
promise_type::unhandled_exception()捕获,默认行为是调用std::terminate()。可自定义处理。
5. 常见问题与解决
| 问题 | 解释 | 解决方案 |
|---|---|---|
| 协程句柄失效 | 调用 resume() 后没有保持句柄 |
在外部保持 std::coroutine_handle,不要在内部销毁 |
| 内存泄漏 | 未调用 destroy() |
在类析构函数中销毁,或者使用 std::optional 自动管理 |
| 无返回值 | co_return 需要匹配返回类型 |
确认 promise_type 的 get_return_object() 返回正确类型 |
| 多线程安全 | 协程本身不是线程安全的 | 在多线程访问前使用互斥锁或设计为线程本地 |
| 调试困难 | 协程状态机难以直接观察 | 通过 -g 生成调试信息,或使用第三方工具 cppinsights |
6. 进一步学习资源
- C++ 官方文档:
std::coroutine和std::experimental::generator说明。 - 书籍:《C++20 Cookbook》有协程章节。
- 博客:多家 C++ 专栏已发布从入门到高级的协程教程。
- 工具:Clang 的
-fsanitize=thread与-fsanitize=address对协程测试非常友好。
7. 结语
协程让异步编程变得更像同步代码,极大简化了逻辑。掌握了 co_await、co_yield、co_return 与 promise_type 的工作机制后,你可以轻松构建高性能网络、游戏或 GUI 相关的异步系统。希望本文能成为你迈入 C++20 并发协程世界的第一步。祝编码愉快!