C++ 的有栈协程和无栈协程有什么区别?
·
在 C++ 中,协程(Coroutine)是一种轻量级的并发机制,允许函数在特定位置挂起(suspend)和恢复(resume)。协程分为 有栈协程(Stackful Coroutine) 和 无栈协程(Stackless Coroutine),它们在实现方式、内存占用、切换开销和适用场景上有显著区别。以下是两者的详细对比:
1. 核心区别概述
| 特性 | 有栈协程(Stackful) | 无栈协程(Stackless) |
|---|---|---|
| 栈管理 | 每个协程有独立的调用栈 | 共享调用栈,仅保存局部变量和挂起点状态 |
| 内存占用 | 高(需预分配固定大小的栈空间) | 低(仅保存必要状态) |
| 切换开销 | 较高(需保存/恢复整个栈) | 极低(仅修改寄存器) |
| 挂起深度 | 可在嵌套函数中任意位置挂起 | 只能在协程函数顶层挂起 |
| 实现复杂度 | 高(需操作系统或库支持) | 低(编译器生成状态机) |
| 典型代表 | Boost.Coroutine、Windows 纤程(Fiber) | C++20 协程、Goroutines(Go语言) |
2. 有栈协程(Stackful Coroutine)
特点
- 独立栈空间:每个协程拥有独立的调用栈,可保存完整的函数调用链。
- 任意挂起:可在任意嵌套函数中挂起(无需协程函数内显式标记)。
- 高灵活性:适合需要深调用栈或复杂控制流的场景。
实现原理
- 协程切换时,需要保存/恢复完整的栈上下文(寄存器、栈指针等)。
- 依赖操作系统提供的上下文切换 API(如
setjmp/longjmp或平台特定的纤程 API)。
示例(Boost.Coroutine)
#include <boost/coroutine/all.hpp>
#include <iostream>
void coro_func(boost::coroutines::coroutine<void>::push_type& yield) {
std::cout << "Start\n";
yield(); // 挂起协程
std::cout << "Resumed\n";
}
int main() {
boost::coroutines::coroutine<void>::pull_type coro(coro_func); // 创建协程
std::cout << "Main\n";
coro(); // 恢复协程
}
输出:
Start
Main
Resumed
优缺点
- 优点:
- 支持任意嵌套函数挂起。
- 更适合复杂控制流(如递归、回调)。
- 缺点:
- 内存占用高(每个协程需预分配栈空间)。
- 切换开销大(保存/恢复完整栈)。
3. 无栈协程(Stackless Coroutine)
特点
- 共享调用栈:协程与调用者共享栈,仅保存局部变量和挂起点状态。
- 显式挂起:必须通过
co_await或co_yield显式标记挂起点。 - 低开销:适合高并发、低延迟场景。
实现原理
- 编译器将协程函数转换为状态机,每个挂起点对应一个状态。
- 协程挂起时,仅保存局部变量和挂起点位置(无需保存完整栈)。
示例(C++20 协程)
#include <iostream>
#include <coroutine>
struct Coro {
struct promise_type {
Coro get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
};
};
Coro coro_func() {
std::cout << "Start\n";
co_await std::suspend_always{}; // 挂起协程
std::cout << "Resumed\n";
}
int main() {
auto coro = coro_func();
std::cout << "Main\n";
coro.handle.resume(); // 恢复协程
}
输出:
Start
Main
Resumed
优缺点
- 优点:
- 内存占用极低(仅保存必要状态)。
- 切换开销极小(无需操作调用栈)。
- 适合大规模并发(如百万级协程)。
- 缺点:
- 无法在嵌套函数中直接挂起(需通过协程适配器)。
- 控制流受限(需显式标记挂起点)。
4. 关键对比
(1) 内存占用
- 有栈协程:每个协程需预分配固定大小的栈(通常几 KB 到几 MB),协程数量受限于内存。
- 无栈协程:仅保存局部变量和挂起点状态(通常几十到几百字节),可支持百万级协程。
(2) 切换性能
- 有栈协程:切换需保存/恢复完整栈,开销较大(微秒级)。
- 无栈协程:切换仅修改寄存器,开销极低(纳秒级)。
(3) 挂起灵活性
- 有栈协程:可在任意函数调用深度挂起(如递归函数中)。
- 无栈协程:只能在协程函数顶层通过
co_await挂起。
(4) 适用场景
- 有栈协程:
- 需要深调用栈或复杂控制流的任务(如游戏 AI、网络协议处理)。
- 依赖第三方库回调的场景(如异步 I/O)。
- 无栈协程:
- 高并发、低延迟任务(如微服务、高频交易)。
- 大规模并行计算(如生成器、流处理)。
5. 如何选择?
- 选择有栈协程:
- 需要任意位置挂起或与现有回调代码集成。
- 可接受较高的内存开销(如数千协程)。
- 选择无栈协程:
- 需要超大规模并发(如百万级协程)。
- 追求极致性能(如延迟敏感型应用)。
总结
- 有栈协程像是“轻量级线程”,灵活但资源消耗较大。
- 无栈协程像是“超级函数”,高效但控制流受限。
C++20 标准选择了无栈协程作为原生支持,因其更适合现代高性能计算的需求。而有栈协程可通过库(如 Boost.Coroutine)实现,用于特定场景。理解两者的区别,能帮助你在实际项目中做出合理选择。
鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。
更多推荐


所有评论(0)