在 C++20 中,协程(coroutines)为异步编程提供了极大的便利。与传统的回调或线程模型相比,协程使代码更易读、维护性更好,且性能更优。下面通过一个最小化示例,演示如何在 C++20 中创建、使用以及优化协程,并提供一些常见的陷阱与最佳实践。
1. 基础概念回顾
- awaiter:实现
await_ready,await_suspend,await_resume的对象。 - generator:最常见的协程形态,用于按需生成值。
- task:封装异步操作,通常返回
std::future或自定义状态。
2. 一个简单的生成器
#include <iostream>
#include <coroutine>
#include <optional>
template <typename T>
struct generator {
struct promise_type {
T current_value;
std::optional <T> value_;
auto 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 unhandled_exception() { std::exit(1); }
template <typename U>
std::suspend_always yield_value(U&& v) {
current_value = std::forward <U>(v);
value_ = current_value;
return {};
}
void return_void() {}
};
std::coroutine_handle <promise_type> handle_;
explicit generator(std::coroutine_handle <promise_type> h) : handle_(h) {}
~generator() { if (handle_) handle_.destroy(); }
bool next() {
if (!handle_.done()) {
handle_.resume();
return !handle_.done();
}
return false;
}
T current() const { return handle_.promise().current_value; }
};
generator <int> range(int start, int end) {
for (int i = start; i < end; ++i)
co_yield i;
}
使用示例:
int main() {
auto gen = range(0, 5);
while (gen.next())
std::cout << gen.current() << " ";
}
输出:0 1 2 3 4
3. 协程与 std::future
虽然协程可以直接返回值,但在现代 C++ 中,最常见的做法是将协程包装成 std::future,实现异步任务。下面给出一个 async_task 的简易实现:
#include <future>
#include <coroutine>
template <typename T>
struct async_task {
struct promise_type {
T result_;
std::exception_ptr eptr_;
auto get_return_object() {
return async_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() { eptr_ = std::current_exception(); }
void return_value(T value) { result_ = std::move(value); }
std::future <T> get_future() {
return std::future <T>(handle_);
}
};
std::coroutine_handle <promise_type> handle_;
explicit async_task(std::coroutine_handle <promise_type> h) : handle_(h) {}
~async_task() { if (handle_) handle_.destroy(); }
std::future <T> get_future() { return handle_.promise().get_future(); }
};
使用:
async_task <int> async_add(int a, int b) {
co_return a + b;
}
int main() {
auto task = async_add(3, 4);
std::future <int> fut = task.get_future();
std::cout << "Result: " << fut.get() << '\n';
}
4. 性能与资源管理
4.1 关闭不必要的挂起
协程的默认 suspend_always 会在每次 co_yield 或 co_return 时挂起。若不需要暂停,可使用 suspend_never,从而避免一次无意义的上下文切换:
std::suspend_never initial_suspend() { return {}; } // 立即开始执行
4.2 std::shared_future 与 task
如果多个消费者需要同一个协程结果,使用 std::shared_future 可避免重复执行:
auto fut = std::shared_future <int>(task.get_future());
4.3 错误传播
协程内部抛出的异常会被 unhandled_exception 捕获并保存为 exception_ptr。用户可以在 future::get() 时重新抛出,保持错误信息完整。
5. 常见陷阱
- 忘记销毁协程句柄:协程句柄必须在不再使用时
destroy(),否则会导致内存泄漏。 - 过度使用
co_yield:如果每次co_yield都需要一次上下文切换,性能会下降。可考虑将值一次性生成或使用批处理。 - 不兼容的返回类型:
co_return必须匹配promise_type::return_value的签名,否则编译失败。
6. 进一步阅读
- 《C++ Coroutines》作者: Herb Sutter, Niall Douglas
- cppreference.com:
std::generator,std::future,std::coroutine_handle
小结
C++20 协程通过让编译器处理挂起与恢复细节,极大简化了异步代码的写法。掌握协程的基础结构(promise、awaiter、handle),并结合 std::future 与资源管理模式,能够让你在需要高并发、低延迟的场景下写出既高效又易读的代码。祝你在 C++ 的协程旅程中愉快前行!