在 C++20 标准中引入了协程(coroutine)这一强大功能,但对于许多开发者而言,直接使用 std::generator 或第三方库(如 Boost.Coroutine)仍然显得繁琐。本文将演示如何用最小的代码量实现一个简易的协程框架,帮助你快速理解协程的工作机制,并在自己的项目中快速实验。
1. 先说说协程的核心概念
协程是一种可暂停、可恢复的函数,内部的状态会在暂停时被保存,下次继续执行时从上次中断的位置恢复。C++20 对协程的支持核心是:
co_await、co_yield、co_return三个关键字。- 悬挂点(promise):协程体内部的执行与外部协作的桥梁。
std::coroutine_handle:可用来手动管理协程的生命周期。
我们会用到 std::promise 结构来实现协程的返回值和状态。
2. 一个最小化的协程包装器
#include <coroutine>
#include <iostream>
#include <exception>
template<typename T>
class Coroutine {
public:
struct promise_type;
using handle_type = std::coroutine_handle <promise_type>;
struct promise_type {
T value_;
std::exception_ptr exception_;
auto get_return_object() {
return Coroutine{handle_type::from_promise(*this)};
}
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { exception_ = std::current_exception(); }
template<typename U>
auto yield_value(U&& v) {
value_ = std::forward <U>(v);
return std::suspend_always{};
}
void return_value(T v) { value_ = std::move(v); }
};
Coroutine(handle_type h) : h_(h) {}
~Coroutine() { if (h_) h_.destroy(); }
bool resume() {
if (!h_.done()) {
h_.resume();
return !h_.done();
}
return false;
}
T get() {
if (h_.promise().exception_)
std::rethrow_exception(h_.promise().exception_);
return h_.promise().value_;
}
private:
handle_type h_;
};
说明
promise_type:保存协程的返回值和异常。initial_suspend与final_suspend:均返回suspend_always,保证协程在co_await前先暂停,结束时也会暂停,方便外部手动调用resume()。yield_value:把协程产出的值写进value_,随后暂停。return_value:协程结束时写入最终结果。
3. 一个简单的协程使用示例
// 生成斐波那契数列的协程
Coroutine <int> fibonacci(int n) {
int a = 0, b = 1;
for (int i = 0; i < n; ++i) {
co_yield a;
int tmp = a;
a = b;
b = tmp + b;
}
co_return a; // 最后一个数
}
int main() {
auto fib = fibonacci(10);
while (fib.resume()) {
std::cout << fib.get() << ' ';
}
std::cout << "final: " << fib.get() << '\n';
return 0;
}
输出
0 1 1 2 3 5 8 13 21 34 final: 55
这里的
fib.get()在resume()调用后会拿到刚刚被co_yield暂停的值。final_suspend()后的resume()会使协程结束,此时get()返回co_return的值。
4. 与标准库协程的整合
C++20 只提供了底层 API,若想更方便地使用,可通过包装 std::generator(实验性)或 std::experimental::generator 实现。下面给出一个更通用的生成器实现:
template<typename T>
class Generator {
public:
struct promise_type {
T value_;
std::exception_ptr exception_;
auto get_return_object() {
return Generator{std::coroutine_handle <promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { exception_ = std::current_exception(); }
template<typename U>
auto yield_value(U&& v) {
value_ = std::forward <U>(v);
return std::suspend_always{};
}
void return_void() {}
};
using handle_type = std::coroutine_handle <promise_type>;
Generator(handle_type h) : h_(h) {}
~Generator() { if (h_) h_.destroy(); }
bool next() {
if (!h_.done()) {
h_.resume();
return !h_.done();
}
return false;
}
T current() {
if (h_.promise().exception_)
std::rethrow_exception(h_.promise().exception_);
return h_.promise().value_;
}
private:
handle_type h_;
};
使用方式与前面相似,区别在于:
return_void():协程不需要返回值,只负责产出。next():继续到下一个yield。current():读取最新的产出值。
5. 常见坑和调试技巧
-
忘记
resume()
协程默认在创建时暂停,需要手动调用resume()才能开始执行。常见错误是直接调用get(),会得到未初始化的值。 -
异常泄漏
若协程内部抛异常,需要在promise_type::unhandled_exception捕获。否则外部无法获取异常信息。 -
多次
resume()后协程已结束
resume()在协程结束后会返回false,此时再调用resume()仍会返回false。可通过handle.done()检查。 -
资源泄漏
handle.destroy()必须在协程完成后调用。我们在Coroutine/Generator的析构中完成。
6. 小结
- C++20 的协程是通过
promise_type与coroutine_handle机制实现的。 - 本文提供了一个最小化的协程框架,支持
co_yield、co_return。 - 示例展示了斐波那契数列生成器,演示了协程的暂停与恢复。
- 对比标准库实验性
generator,可根据项目需求自行选择实现方式。
使用协程能够让你以更直观的方式处理异步/生成器逻辑,建议在高并发或事件驱动场景下尝试。祝编码愉快!