协程到底解决的是什么问题?适用那些场景?一个best practice
一句话:协程解决的是”等待”期间如何不浪费线程的问题。
要理解这句话,先看没有协程时会发生什么。
问题的根源:I/O 比 CPU 慢一万倍
操作 耗时 L1 缓存读取 ~1 ns 内存读取 ~100 ns SSD 随机读 ~100,000 ns (0.1 ms) 网络请求(同城) ~500,000 ns (0.5 ms) 数据库查询 ~1,000,000 ns (1 ms)
一个线程在等待网络响应的 1ms 里,能执行大约 100 万条 CPU 指令。这些 CPU 时间全部浪费掉了。
三种应对方案,各有代价
方案一:同步阻塞
string result = http_get("/api/data"); // 线程在这里睡觉
process(result);
最简单,但线程在 I/O 期间完全被占住。如果有 1 万个并发请求,就需要 1 万个线程——内存爆炸(每个线程默认 8MB 栈),上下文切换开销极大。
方案二:回调地狱
http_get("/api/user", [](User user) {
http_get("/api/orders?id=" + user.id, [user](Orders orders) {
db_query("INSERT ...", [](Result r) {
// 嵌套第四层...
// 错误处理在哪?
// 循环怎么写?
});
});
});
线程不阻塞了,但代码失去了线性结构,错误处理、循环、早退都变得极其麻烦。这就是著名的”回调地狱”。
方案三:协程
User user = co_await http_get("/api/user");
Orders orders = co_await http_get("/api/orders?id=" + user.id);
Result r = co_await db_query("INSERT ...");
代码写起来像同步,执行起来像异步。co_await 的含义是:”挂起我这个协程,让出线程去干别的事,等结果好了再回来继续”。
这才是协程的本质价值:用同步的写法,得到异步的性能。
协程 vs 线程的根本区别
线程的切换由操作系统决定,是抢占式的,切换成本高(保存/恢复寄存器、TLB 刷新、内核态切换,约 1-10 μs)。
协程的切换由程序员(或运行时)决定,是协作式的,切换成本极低(只需保存少量寄存器,约 10-100 ns),因为切换点是明确的 co_await,不需要保存完整的 CPU 状态。
10,000 个并发连接: 线程模型:10,000 个线程 × 8MB 栈 = 80GB 内存(不可能) 协程模型:10,000 个协程 × ~200B 帧 = 2MB 内存(没问题)
适用场景
核心判断标准:你的瓶颈是 I/O 还是 CPU?
协程只对 I/O 密集型有效。CPU 密集型(图像处理、加密、压缩)用协程没有意义,应该用线程或进程并行。
最适合的场景
网络服务器 / API 网关:每个请求需要调用多个下游服务,大量时间在等网络。这是协程最主流的用武之地——Nginx、Node.js、Go 的 HTTP 服务器本质都是这个模型。
数据库访问层:查询要等数据库,连接池配合协程可以用少量线程服务大量并发查询。
爬虫和批量 HTTP 请求:同时发出几千个请求,等响应回来再处理,比多线程高效得多。
实时通信系统(WebSocket、长连接):每个连接大部分时间是空闲的,等用户发消息,协程可以让一个线程维护数万个连接。
游戏逻辑脚本:技能释放、剧情对话这类”等待几秒再执行下一步”的逻辑,用协程比状态机清晰得多。
不适合的场景
- 纯 CPU 计算(协程帮不了你,用线程/SIMD)
- 代码必须运行在不支持堆分配的硬件上(协程帧在堆上)
- 简单的单次操作(没有并发需求时引入协程只增加复杂度)
Best Practice:一个完整可运行的例子
用 Asio 写一个并发 HTTP 请求聚合器——同时发出多个请求,等所有结果回来后合并,这是最典型的协程使用模式。
// 依赖:asio (standalone,不需要 Boost)
// 编译:g++ -std=c++20 -o demo demo.cpp -I/path/to/asio/include
#include <asio.hpp>
#include <asio/co_spawn.hpp>
#include <asio/detached.hpp>
#include <asio/use_awaitable.hpp>
#include <chrono>
#include <iostream>
#include <vector>
using namespace asio;
using namespace std::chrono_literals;
// -------------------------------------------------------
// 模拟一次异步操作(真实场景换成 async_connect/async_read)
// -------------------------------------------------------
awaitable<std::string> fetch(io_context& ctx, std::string name, int delay_ms) {
// 用 timer 模拟网络延迟
steady_timer timer(ctx);
timer.expires_after(std::chrono::milliseconds(delay_ms));
co_await timer.async_wait(use_awaitable);
co_return "result_from_" + name;
}
// -------------------------------------------------------
// 串行:一个接一个等
// -------------------------------------------------------
awaitable<void> serial_requests(io_context& ctx) {
auto t0 = std::chrono::steady_clock::now();
// 三个请求依次等待,总时间 = 100 + 200 + 150 = 450ms
auto r1 = co_await fetch(ctx, "service_A", 100);
auto r2 = co_await fetch(ctx, "service_B", 200);
auto r3 = co_await fetch(ctx, "service_C", 150);
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - t0).count();
std::cout << "[Serial] " << r1 << ", " << r2 << ", " << r3
<< " — " << elapsed << "ms\n";
}
// -------------------------------------------------------
// 并发:同时发出,等最慢的那个
// -------------------------------------------------------
awaitable<void> concurrent_requests(io_context& ctx) {
auto t0 = std::chrono::steady_clock::now();
// experimental::make_parallel_group 或直接用 co_spawn + channel
// 这里用最简洁的 awaitable<> 组合方式:
// 同时启动三个协程,使用 awaitable_operators 等待全部完成
using asio::experimental::awaitable_operators::operator&&;
auto [r1, r2, r3] = co_await (
fetch(ctx, "service_A", 100) &&
fetch(ctx, "service_B", 200) &&
fetch(ctx, "service_C", 150)
);
// 总时间 ≈ max(100, 200, 150) = 200ms
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - t0).count();
std::cout << "[Concurrent] " << r1 << ", " << r2 << ", " << r3
<< " — " << elapsed << "ms\n";
}
// -------------------------------------------------------
// 带超时:任意一个先完成就继续
// -------------------------------------------------------
awaitable<void> with_timeout(io_context& ctx) {
using asio::experimental::awaitable_operators::operator||;
steady_timer timeout_timer(ctx);
timeout_timer.expires_after(120ms);
// fetch 和 timeout 竞争,谁先完成用谁的结果
auto result = co_await (
fetch(ctx, "slow_service", 300) ||
timeout_timer.async_wait(use_awaitable)
);
if (result.index() == 0)
std::cout << "[Timeout] Got: " << std::get<0>(result) << "\n";
else
std::cout << "[Timeout] Timed out after 120ms\n";
}
// -------------------------------------------------------
// 带错误处理的协程
// -------------------------------------------------------
awaitable<void> with_error_handling(io_context& ctx) {
try {
auto r = co_await fetch(ctx, "flaky_service", 50);
std::cout << "[ErrorHandling] Success: " << r << "\n";
} catch (const std::exception& e) {
// co_await 抛出异常就像同步代码一样自然
std::cout << "[ErrorHandling] Failed: " << e.what() << "\n";
}
}
// -------------------------------------------------------
// 带重试的协程——注意这里循环写起来多自然
// -------------------------------------------------------
awaitable<std::string> fetch_with_retry(io_context& ctx,
std::string name,
int max_retries) {
for (int i = 0; i < max_retries; i++) {
try {
co_return co_await fetch(ctx, name, 50);
} catch (...) {
if (i == max_retries - 1) throw; // 最后一次失败就抛出
std::cout << " retry " << i + 1 << "...\n";
steady_timer backoff(ctx);
backoff.expires_after(std::chrono::milliseconds(100 * (i + 1)));
co_await backoff.async_wait(use_awaitable);
}
}
throw std::runtime_error("unreachable");
}
// -------------------------------------------------------
// 主入口
// -------------------------------------------------------
int main() {
io_context ctx;
// 注意:所有协程都跑在同一个线程上
// 如果需要多线程,用 ctx.run() 在多个线程里同时调用
co_spawn(ctx, serial_requests(ctx), detached);
co_spawn(ctx, concurrent_requests(ctx), detached);
co_spawn(ctx, with_timeout(ctx), detached);
co_spawn(ctx, with_error_handling(ctx), detached);
ctx.run(); // 事件循环,直到所有协程完成
return 0;
}
运行结果大致是:
[Serial] result_from_service_A, result_from_service_B, result_from_service_C — 450ms [Concurrent] result_from_service_A, result_from_service_B, result_from_service_C — 200ms [Timeout] Timed out after 120ms [ErrorHandling] Success: result_from_flaky_service
串行 450ms,并发 200ms——同样是三个请求,时间缩短了一半多,而且只用了一个线程。
几条真正的 Best Practice
1. 永远不要在协程里调用同步阻塞操作。
std::this_thread::sleep_for()、同步 read()、mutex::lock() 会阻塞整个线程,其他协程全部卡死。所有阻塞操作必须换成异步版本。
2. 用 && 并发,不要串行 co_await。
如果两个操作没有依赖关系,永远用并发模式。串行 co_await 是最常见的性能陷阱。
3. 错误处理用 try/catch,不要用回调式 error_code。
协程最大的人体工学优势就是可以用 try/catch 写异常处理,不要放弃这个优势。
4. 取消和超时是必须考虑的,不是可选的。
永远不要写没有超时的网络请求,用 || 竞争一个 timer 是最简单的超时模式。
5. 保持协程函数短小,单一职责。
协程的生命周期管理比普通函数复杂,函数越长越难推理。把复杂逻辑拆成多个 co_await 的小协程。
6. 注意对象生命周期。
协程在 co_await 点挂起期间,调用者可能已经析构了你捕获的引用。捕获引用时一定要确认对象生命周期长于协程。
最后一张图
时间轴 →
线程 1 [请求A处理] [等待A的I/O......................] [处理A结果]
↑ 传统阻塞:线程一直被占用
线程 1 [请求A发出] [请求B发出] [请求C发出] [A回来处理] [B回来处理] [C回来处理]
↑ 协程:I/O期间线程去处理其他事,吞吐量大幅提升
协程不让你的代码跑得更快,而是让同样的硬件同时做更多事。这是它的本质价值,也是它的边界。
