协程(Coroutines)是一种轻量级的协作式多任务实现方式,能够让函数在执行过程中暂停并恢复,极大地简化异步编程和生成器的实现。自 C++20 起,标准库已经提供了协程支持,但在实际项目中,很多人仍然倾向于使用第三方协程库或自己实现微协程。本文将从语言特性、标准库实现、常见错误以及如何自行实现一个简单协程框架等方面进行详细解析。
1. C++20 协程的基本语法
C++20 引入了 co_await、co_yield、co_return 三个关键字,配合 std::future、std::generator 等类型实现协程。一个最小的协程示例:
#include <iostream>
#include <coroutine>
#include <thread>
#include <chrono>
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void unhandled_exception() { std::terminate(); }
void return_void() {}
};
};
Task simpleCoroutine() {
std::cout << "Start\n";
co_return;
}
int main() {
simpleCoroutine();
return 0;
}
此协程立即执行到结束,因为 initial_suspend 和 final_suspend 都返回 suspend_never。
2. 协程与传统异步模型的区别
| 传统异步 | 协程 |
|---|---|
| 需要回调函数 | 代码可读性高,像同步代码 |
| 线程/事件循环 | 轻量级协作 |
| 错误处理复杂 | 通过异常传播 |
协程的核心优势在于把“暂停点”显式化,消除了回调金字塔。
3. 常见错误与调试技巧
-
忘记
co_return
协程没有co_return会导致main返回未定义状态。
调试技巧:使用编译器警告-Wreturn-type。 -
错误的 Promise 实现
promise_type的生命周期与协程的生命周期紧密相关。
调试技巧:在promise_type构造和析构中打印日志。 -
忘记
std::suspend_always/suspend_never
让协程一直挂起或一直执行,导致无限循环。
调试技巧:使用std::suspend_always作为默认暂停策略。 -
栈溢出
过深的递归协程会导致栈溢出。
调试技巧:限制递归深度,或改用迭代。
4. 自己实现一个简易协程框架
虽然标准库提供了完整实现,但有时我们想要更细粒度的控制,例如自定义协程调度器。下面给出一个最小化协程框架的实现,支持 yield 与 await。
4.1 基础类型
#include <functional>
#include <vector>
#include <memory>
class Coroutine;
using Task = std::function<void(Coroutine&)>;
class Coroutine {
public:
Coroutine(Task func) : func_(std::move(func)), finished_(false) {}
void resume() {
if (!finished_) func_(*this);
}
void yield() {
// 暂停当前协程,控制权交给调度器
// 这里简单实现直接返回
}
void finish() { finished_ = true; }
bool finished() const { return finished_; }
private:
Task func_;
bool finished_;
};
4.2 调度器
class Scheduler {
public:
void add(Coroutine::Task t) {
coros_.emplace_back(std::make_shared <Coroutine>(std::move(t)));
}
void run() {
while (!coros_.empty()) {
auto c = coros_.front();
c->resume();
if (c->finished()) {
coros_.erase(coros_.begin());
}
}
}
private:
std::vector<std::shared_ptr<Coroutine>> coros_;
};
4.3 示例:生成斐波那契数列
int main() {
Scheduler sched;
sched.add([](Coroutine& self) {
int a = 0, b = 1;
for (int i = 0; i < 10; ++i) {
std::cout << a << " ";
int next = a + b;
a = b;
b = next;
self.yield(); // 暂停
}
self.finish();
});
sched.run();
}
该框架非常基础,缺少异常传播、状态保存、协程间通信等高级特性。但它展示了协程的核心概念:一个协程是一个可暂停、可恢复的执行单元,调度器负责管理其生命周期。
5. 进一步学习与实践
- Boost.Coroutine:成熟的协程库,支持多种调度策略。
- Libco:轻量级协程库,适合嵌入式或高并发场景。
- C++20 协程实验室:在现代 IDE 中尝试
co_await与co_yield。
实践建议:先在一个小项目中使用标准协程实现异步 IO,然后逐步尝试自己实现调度器,了解协程的底层机制。
6. 小结
C++20 的协程为异步编程提供了强大的工具。掌握 co_await、co_yield 的使用后,编写异步代码会像写同步代码一样自然。标准库提供的 std::generator、std::future 等类型已经足够满足大多数需求,但对细粒度控制有需求的项目,可以尝试自己实现轻量级协程框架。通过本文的代码示例与调试技巧,读者可以快速上手并深入理解协程的本质。
祝编码愉快,协程无极限!