Appearance
有栈协程 (Stackful Coroutines) 和无栈协程 (Stackless Coroutines) 是实现协程的两种主要方式,它们在如何管理协程的执行上下文(尤其是调用栈)方面有根本区别。
理解核心:协程的本质是在单线程内实现并发,允许函数(协程)在执行过程中暂停(yield
),然后稍后从暂停点恢复(resume
)。关键在于暂停和恢复时如何保存和恢复函数的状态。
有栈协程 (Stackful Coroutines)
- 核心特点:每个协程都有自己独立的、完整的调用栈(Stack)。
- 工作原理:
- 当一个有栈协程被创建时,会为其分配一块独立的内存作为其调用栈。
- 当协程
yield
(暂停)时,它当前的整个调用栈(包括所有局部变量、函数调用层级、返回地址等)都会被完整地保存下来。 - 当协程
resume
(恢复)时,之前保存的调用栈会被完整地恢复,程序计数器指向暂停点,协程从上次离开的地方无缝继续执行,就好像它从未离开过一样。 - 关键点:协程可以在其调用的任何函数内部暂停,即使这个函数不是专门为协程设计的(即普通的同步函数)。因为整个调用栈都被保留了。
- 优点:
- 编程简单:代码写起来更像传统的同步代码,可以在任意深度的函数调用中
yield
,不需要特殊的关键字(如async/await
)来“传染”整个调用链。 - 更强的表达能力:可以在库函数或者回调函数中自然地挂起。
- 编程简单:代码写起来更像传统的同步代码,可以在任意深度的函数调用中
- 缺点:
- 内存开销较大:每个协程都需要一个独立的栈,即使是很小的栈,当协程数量巨大时,总的内存消耗也会很可观。
- 上下文切换可能稍慢:保存和恢复整个栈的开销相对较大(尽管通常比线程切换快得多)。
- 典型例子:
- Go 语言的 Goroutine
- Lua 语言的 Coroutine
- C++ Boost.Coroutine (旧版,新版 ASIO 和 Coroutine TS 更偏向无栈)
- 一些 Fiber 库实现
无栈协程 (Stackless Coroutines)
- 核心特点:协程不拥有自己独立的调用栈。它们通常在调用者的栈上执行,或者其状态被保存在堆上分配的某个对象(如状态机)中。
- 工作原理:
- 通常依赖编译器或语言运行时的特殊支持(例如
async/await
关键字)。 - 当一个
async
函数遇到await
(暂停点)时,编译器会将函数的状态(主要是需要跨越await
保存的局部变量)打包到一个状态机对象中(通常在堆上分配)。然后函数返回一个表示未来结果的对象(如Promise
或Future
)。 - 当
await
的操作完成,事件循环会调用状态机的特定方法,恢复之前保存的状态,并从await
之后的地方继续执行。 - 关键点:协程只能在显式标记的暂停点(如
await
或yield
关键字处)暂停,并且通常只能在协程函数自身内部,或者它调用的另一个async
函数内部暂停。不能在一个普通的、非async
的同步函数内部暂停当前协程。
- 通常依赖编译器或语言运行时的特殊支持(例如
- 优点:
- 内存开销小:不需要为每个协程分配独立的栈,状态机对象通常比完整栈小得多。可以支持创建非常大量的协程。
- 上下文切换快:仅保存和恢复少量状态,切换开销小。
- 缺点:
- “函数颜色”问题 (Function Coloring):
async
函数具有“传染性”。一旦你开始使用async/await
,很多相关的函数也需要变成async
,这可能导致代码库的大面积修改。一个普通函数不能直接await
一个async
函数的结果而不改变自身。 - 编程模型限制:只能在特定的点暂停,不能在任意函数调用中暂停。
- “函数颜色”问题 (Function Coloring):
- 典型例子:
- Python 的
async/await
(基于生成器实现) - JavaScript 的
async/await
- C# 的
async/await
- Kotlin 的 Coroutines (主要是无栈,但通过一些技巧可以和阻塞代码交互)
- Rust 的
async/await
- Python 的
总结对比:
特性 | 有栈协程 (Stackful) | 无栈协程 (Stackless) |
---|---|---|
栈管理 | 每个协程有独立栈 | 共享调用者栈 / 状态保存在堆上 |
内存占用 | 较大 (每个协程一个栈) | 较小 (状态机对象) |
暂停点 | 可在任意函数调用层级暂停 | 只能在显式标记点 (await /yield ) 暂停 |
编程模型 | 类似同步代码,侵入性小 | 需要 async/await 等关键字,有“函数颜色” |
实现复杂度 | 对运行时或库要求较高 | 对编译器和语言特性要求较高 |
适用场景 | 需要高度灵活性,协程数量可控 | 需要海量并发,I/O密集型任务 |
例子 | Go Goroutine, Lua Coroutine | Python/JS/C# async/await |
简单来说:
- 有栈协程 更强大灵活,编程体验更接近传统同步代码,但代价是内存。
- 无栈协程 更轻量高效,适合大规模并发,但对编程范式有一定约束。
现代语言趋势似乎更偏向无栈协程,因为它们在Web服务器等高并发I/O密集型场景下表现出色,并且编译器技术的发展使得 async/await
的体验越来越好。而Go语言的Goroutine则是一个非常成功的有栈协程的例子,它通过高度优化的调度器和较小的初始栈(可动态增长)来平衡性能和内存开销。