Skip to content

有栈协程 (Stackful Coroutines) 和无栈协程 (Stackless Coroutines) 是实现协程的两种主要方式,它们在如何管理协程的执行上下文(尤其是调用栈)方面有根本区别。

理解核心:协程的本质是在单线程内实现并发,允许函数(协程)在执行过程中暂停(yield),然后稍后从暂停点恢复(resume)。关键在于暂停和恢复时如何保存和恢复函数的状态。

  1. 有栈协程 (Stackful Coroutines)

    • 核心特点:每个协程都有自己独立的、完整的调用栈(Stack)。
    • 工作原理
      • 当一个有栈协程被创建时,会为其分配一块独立的内存作为其调用栈。
      • 当协程 yield(暂停)时,它当前的整个调用栈(包括所有局部变量、函数调用层级、返回地址等)都会被完整地保存下来。
      • 当协程 resume(恢复)时,之前保存的调用栈会被完整地恢复,程序计数器指向暂停点,协程从上次离开的地方无缝继续执行,就好像它从未离开过一样。
      • 关键点:协程可以在其调用的任何函数内部暂停,即使这个函数不是专门为协程设计的(即普通的同步函数)。因为整个调用栈都被保留了。
    • 优点
      • 编程简单:代码写起来更像传统的同步代码,可以在任意深度的函数调用中 yield,不需要特殊的关键字(如 async/await)来“传染”整个调用链。
      • 更强的表达能力:可以在库函数或者回调函数中自然地挂起。
    • 缺点
      • 内存开销较大:每个协程都需要一个独立的栈,即使是很小的栈,当协程数量巨大时,总的内存消耗也会很可观。
      • 上下文切换可能稍慢:保存和恢复整个栈的开销相对较大(尽管通常比线程切换快得多)。
    • 典型例子
      • Go 语言的 Goroutine
      • Lua 语言的 Coroutine
      • C++ Boost.Coroutine (旧版,新版 ASIO 和 Coroutine TS 更偏向无栈)
      • 一些 Fiber 库实现
  2. 无栈协程 (Stackless Coroutines)

    • 核心特点:协程不拥有自己独立的调用栈。它们通常在调用者的栈上执行,或者其状态被保存在堆上分配的某个对象(如状态机)中。
    • 工作原理
      • 通常依赖编译器或语言运行时的特殊支持(例如 async/await 关键字)。
      • 当一个 async 函数遇到 await(暂停点)时,编译器会将函数的状态(主要是需要跨越 await 保存的局部变量)打包到一个状态机对象中(通常在堆上分配)。然后函数返回一个表示未来结果的对象(如 PromiseFuture)。
      • await 的操作完成,事件循环会调用状态机的特定方法,恢复之前保存的状态,并从 await 之后的地方继续执行。
      • 关键点:协程只能在显式标记的暂停点(如 awaityield 关键字处)暂停,并且通常只能在协程函数自身内部,或者它调用的另一个 async 函数内部暂停。不能在一个普通的、非 async 的同步函数内部暂停当前协程。
    • 优点
      • 内存开销小:不需要为每个协程分配独立的栈,状态机对象通常比完整栈小得多。可以支持创建非常大量的协程。
      • 上下文切换快:仅保存和恢复少量状态,切换开销小。
    • 缺点
      • “函数颜色”问题 (Function Coloring)async 函数具有“传染性”。一旦你开始使用 async/await,很多相关的函数也需要变成 async,这可能导致代码库的大面积修改。一个普通函数不能直接 await 一个 async 函数的结果而不改变自身。
      • 编程模型限制:只能在特定的点暂停,不能在任意函数调用中暂停。
    • 典型例子
      • Python 的 async/await (基于生成器实现)
      • JavaScript 的 async/await
      • C# 的 async/await
      • Kotlin 的 Coroutines (主要是无栈,但通过一些技巧可以和阻塞代码交互)
      • Rust 的 async/await

总结对比

特性有栈协程 (Stackful)无栈协程 (Stackless)
栈管理每个协程有独立栈共享调用者栈 / 状态保存在堆上
内存占用较大 (每个协程一个栈)较小 (状态机对象)
暂停点可在任意函数调用层级暂停只能在显式标记点 (await/yield) 暂停
编程模型类似同步代码,侵入性小需要 async/await 等关键字,有“函数颜色”
实现复杂度对运行时或库要求较高对编译器和语言特性要求较高
适用场景需要高度灵活性,协程数量可控需要海量并发,I/O密集型任务
例子Go Goroutine, Lua CoroutinePython/JS/C# async/await

简单来说:

  • 有栈协程 更强大灵活,编程体验更接近传统同步代码,但代价是内存。
  • 无栈协程 更轻量高效,适合大规模并发,但对编程范式有一定约束。

现代语言趋势似乎更偏向无栈协程,因为它们在Web服务器等高并发I/O密集型场景下表现出色,并且编译器技术的发展使得 async/await 的体验越来越好。而Go语言的Goroutine则是一个非常成功的有栈协程的例子,它通过高度优化的调度器和较小的初始栈(可动态增长)来平衡性能和内存开销。