在 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_awaitco_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)实现,用于特定场景。理解两者的区别,能帮助你在实际项目中做出合理选择。

Logo

鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。

更多推荐