17、协程的高级应用:协程与异步I/O、生成器(Generator)、协程框架的使用
说实话,协程这个话题,很多C++开发者是又爱又怕。爱的是它那近乎魔法般的异步能力,怕的是那堆co_await、co_yield关键字看着就头大。我当年第一次接触C++20协程时,也是翻来覆去看了好几遍标准文档才摸到门道。今天咱们就把协程的高级应用掰开揉碎,聊聊它在异步I/O、生成器以及实际框架中到底怎么用。
17.1 协程与异步I/O:让等待不再阻塞
先说说异步I/O。你想想看,传统的同步I/O,比如读个文件、发个网络请求,线程就得傻等着。这在高并发场景下简直是灾难。我曾在做一个高并发网络服务时,用线程池处理I/O,结果上下文切换开销大得吓人。后来换成协程,问题迎刃而解。
协程的核心优势在于:它能把异步回调“扁平化”成同步写法。说白了,就是代码看起来是顺序执行的,但底层却是非阻塞的。
关键点:协程挂起时,线程可以去做别的事。等I/O完成了,协程再恢复执行。这比线程切换轻量得多。
来看一个简单的异步读文件例子:
#include <experimental/coroutine>
#include <iostream>
#include <thread>
#include <chrono>
// 一个模拟的异步I/O操作
struct AsyncRead {
bool await_ready() { return false; }
void await_suspend(std::experimental::coroutine_handle<> h) {
// 模拟异步:在新线程里执行,完成后恢复协程
std::thread([h]() {
std::this_thread::sleep_for(std::chrono::seconds(1));
h.resume();
}).detach();
}
int await_resume() { return 42; } // 模拟读取到的数据
};
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::experimental::suspend_never initial_suspend() { return {}; }
std::experimental::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};
Task reader() {
std::cout << "开始读取..." << std::endl;
int data = co_await AsyncRead{};
std::cout << "读取完成,数据: " << data << std::endl;
}
int main() {
reader();
std::this_thread::sleep_for(std::chrono::seconds(2));
return 0;
}
你看,co_await 就像一道分水岭。在它之前,协程在运行;在它之后,协程挂起了。等异步操作完成,h.resume() 一调用,协程又活过来了。整个过程,线程没有被阻塞。
我的经验:实际项目中,千万别自己手写这些 awaitable 对象。用现成的库,比如 boost::asio 或者 libuv 的协程封装,它们已经把底层细节处理好了。我曾经手写过一套,调试起来那叫一个酸爽。
17.2 生成器(Generator):懒加载的优雅实现
生成器这个概念,如果你用过Python,肯定不陌生。C++20协程让生成器变得异常简单。生成器说白了就是:每次调用,只生成下一个值,而不是一次性生成全部。这在处理无限序列或大数据集时特别有用。
我举个例子。假设你要生成斐波那契数列,传统做法是生成一个vector,但如果你只要前10个,后面的就浪费了。用生成器,要一个给一个,多省事。
#include <experimental/coroutine>
#include <exception>
#include <memory>
template<typename T>
struct Generator {
struct promise_type {
T current_value;
std::exception_ptr exception;
Generator get_return_object() {
return Generator{
std::experimental::coroutine_handle<promise_type>::from_promise(*this)
};
}
std::experimental::suspend_always initial_suspend() { return {}; }
std::experimental::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { exception = std::current_exception(); }
std::experimental::suspend_always yield_value(T value) {
current_value = value;
return {};
}
};
std::experimental::coroutine_handle<promise_type> coro;
explicit Generator(std::experimental::coroutine_handle<promise_type> h) : coro(h) {}
~Generator() { if (coro) coro.destroy(); }
Generator(const Generator&) = delete;
Generator& operator=(const Generator&) = delete;
Generator(Generator&& other) noexcept : coro(other.coro) {
other.coro = nullptr;
}
bool next() {
if (!coro) return false;
coro.resume();
if (coro.done()) return false;
return true;
}
T value() {
return coro.promise().current_value;
}
};
Generator<int> fibonacci() {
int a = 0, b = 1;
while (true) {
co_yield a;
auto next = a + b;
a = b;
b = next;
}
}
int main() {
auto gen = fibonacci();
for (int i = 0; i < 10; ++i) {
gen.next();
std::cout << gen.value() << " ";
}
// 输出: 0 1 1 2 3 5 8 13 21 34
return 0;
}
这里的关键是 co_yield。它把当前值存到 promise 里,然后挂起协程。每次调用 next(),协程恢复执行,直到遇到下一个 co_yield 再挂起。这种“拉取式”的数据流,在管道处理、流式计算中非常常见。
注意:生成器协程一旦 final_suspend 返回 suspend_never,协程帧会在结束时自动销毁。但如果你返回 suspend_always,就必须手动调用 coro.destroy(),否则会内存泄漏。我刚开始就踩过这个坑,排查了好久才发现是协程帧没释放。
17.3 协程框架的使用:别重复造轮子
说实话,C++20标准库提供的协程支持非常底层。它只给了你 co_await、co_yield、co_return 这三个关键字,以及 promise_type 这个接口。真正好用的协程框架,还得靠第三方库。
我个人比较推荐两个:
- cppcoro:一个轻量级的协程库,提供了
task、generator、async_mutex、async_auto_reset_event等实用组件。适合中小型项目。 - boost::asio:它的协程支持非常成熟,尤其是
co_spawn和awaitable,在网络编程中几乎是标配。
来看一个用 cppcoro 风格的异步任务示例(伪代码,展示思路):
// 假设使用类似 cppcoro 的库
cppcoro::task<int> fetch_data() {
// 异步请求
auto response = co_await http_client->get("https://api.example.com/data");
co_return response.status_code;
}
cppcoro::task<> process() {
auto code = co_await fetch_data();
std::cout << "状态码: " << code << std::endl;
}
int main() {
cppcoro::sync_wait(process());
return 0;
}
你看,代码写起来就像同步一样。但底层,co_await 会挂起当前协程,让线程去处理其他任务。等 HTTP 响应回来了,协程再恢复。这就是协程框架的魅力。
避坑指南:我曾经在一个项目里,试图自己封装一个协程调度器。结果发现,要处理好线程池、任务窃取、异常传播这些细节,工作量远超预期。后来老老实实用了 boost::asio,一周就搞定了。所以我的建议是:除非你在做框架本身,否则直接用成熟的库。
17.4 协程的核心逻辑:一张图说清楚
说了这么多,咱们用一张图来总结协程的生命周期和核心交互。这张图我画了很多遍,每次给团队新人讲协程,都靠它。
这张图展示了协程最核心的流程:主协程发起 co_await,协程帧保存状态并挂起,线程被释放去做其他事。异步I/O完成后,回调函数调用 resume(),协程从挂起点恢复执行,直到 co_return 结束。整个过程,线程没有被阻塞,这就是协程高效的原因。
17.5 总结与个人体会
协程的高级应用,说白了就是三件事:异步I/O的扁平化、生成器的懒加载、以及框架的合理选用。
我个人的体会是,协程最大的价值不在于它有多快(虽然确实比线程切换快),而在于它让异步代码变得可读、可维护。你想想看,一个复杂的异步流程,如果用回调嵌套,代码会变成“回调地狱”。但用协程,就是顺序写下来,跟同步代码一样清晰。
最后提醒一句:协程虽好,但不要滥用。如果你的场景是CPU密集型计算,协程帮不了你,该用线程池还是得用。协程最适合的是I/O密集型任务,比如网络服务、文件读写、数据库查询等。选对工具,才能事半功倍。
公众号:蓝海资料掘金营,微信deep3321