协程(Coroutine)介绍及其分类

1. 什么是协程?

协程也可以叫做”轻量级的线程,用户线程“。
简单的理解协程,协程一种执行过程中能够yield(暂停)和resume(恢复)的子程序,也可以说是协程就是函数和函数运行状态的组合,怎么理解?正常的函数在执行中是直接就执行完成了中间不会有多余的步骤,更不会说我这个函数执行到一半就去执行其他函数了,但是协程不一样,我们使用协程首先要绑定一个入口函数,并且可以在函数的任意位置暂停去执行其他其他的函数,再回来执行暂停的函数,所以说协程是函数和函数运行状态的组合(协程需要绑定入口函数,协程记录了函数的运行状态)。
那么协程是如何做到让函数暂停和让函数的恢复呢?
这个是因为协程的记录会有协程上下文,协程执行yield的时候,协程上下文记录了协程暂停的位置。当resume的时候就是从暂停的地方恢复。协程上下文包含了函数在当前状态的全部cpu寄存器的值,这些寄存器记录函数的栈帧、代码执行的位置等信息,如果把这些值交给cpu去执行,那么就会实现从函数暂停的地方去恢复执行。

需要注意单线程的情况下,协程的resume和yield一定是同步的,一个协程进行yield暂停,必然对应另一个协程的resume恢复,因为线程不能没用执行主体。并且协程的yield和resume是应用程序控制的,这点和线程不一样。

线程的运行和调度是操作系统来完成的,协程的运行和调用是由应用程序来完成的,所以协程也叫做“用户态线程”。

核心特点

  • 协作式调度:协程在特定点(yield 或 await)主动挂起,交出控制权。
  • 上下文切换:保存当前执行状态(如栈帧、寄存器),恢复时继续执行。
  • 轻量级:比线程更低的内存和切换开销。

应用场景

  • 异步 I/O(如网络请求、文件操作)。
  • 事件驱动系统(如游戏循环、Web 服务器)。
  • 并发任务处理(如协程池替代线程池)。

2. 协程的分类

协程可以根据调度方式、栈管理方式等分为以下几类:

2.1 对称协程 vs 非对称协程

对称协程(Symmetric Coroutine)

  • 定义:协程之间可以直接切换,调用者和被调用者的地位平等,控制权可任意传递给另一个协程。
  • 机制:通过类似 yield to 的操作,协程直接指定下一个执行的协程。
  • 示例:Lua 的协程(coroutine.resumecoroutine.yield)。
  • 优点
    • 灵活性高:协程可以自由切换,适合复杂控制流。
    • 通用性:可实现多种并发模型。
  • 缺点
    • 调度复杂:需要程序员显式管理协程切换,易出错。
    • 调试困难:控制流分散,难以跟踪状态。

非对称协程(Asymmetric Coroutine)

  • 定义:协程分为调用者和被调用者,控制权只能在调用者和被调用者之间传递(通常通过 yield 返回调用者,resume 恢复协程)。
  • 机制:协程只能挂起回调用者,无法直接切换到其他协程。
  • 示例:C++20 的 co_awaitco_yield,Python 的 async/await
  • 优点
    • 简单易用:控制流清晰,调用者管理协程生命周期。
    • 易于调试:挂起和恢复的点明确,适合异步编程。
  • 缺点
    • 灵活性较低:无法直接在协程间切换,限制了某些并发模型。
    • 依赖调用者:协程的执行依赖调用者的调度。

对称 vs 非对称对比

  • 对称协程适合需要复杂控制流的场景,如状态机或协作式多任务。
  • 非对称协程更适合异步编程,结构清晰,常用于现代语言(如 C++、Python)。

2.2 有栈协程 vs 无栈协程

有栈协程(Stackful Coroutine)

  • 定义:每个协程拥有独立的调用栈,保存完整的上下文(函数调用链、局部变量等)。
  • 机制:协程暂停时,栈帧完整保存,恢复时直接继续执行。
  • 示例:Boost.Coroutine(C++)、Lua 协程。
  • 优点
    • 灵活性高:可在任意函数调用深度挂起,支持复杂逻辑。
    • 语言无关:无需语言级支持,可在库中实现。
  • 缺点
    • 内存开销大:每个协程需要独立的栈(通常 KB 级别)。
    • 实现复杂:需要管理栈分配和切换,可能涉及汇编或系统调用。
    • 性能开销:上下文切换成本高于无栈协程。

无栈协程(Stackless Coroutine)

  • 定义:协程不维护完整调用栈,仅保存挂起点的最小上下文(如局部变量、状态机)。
  • 机制:编译器将协程转换为状态机,挂起时保存状态,恢复时跳转到对应状态。
  • 示例:C++20 的 co_await/co_yield、Python 的 async/await
  • 优点
    • 内存开销低:仅存储必要状态,适合高并发(如百万级协程)。
    • 高效切换:上下文切换成本低,接近函数调用。
    • 编译器优化:编译器生成状态机,代码高效。
  • 缺点
    • 灵活性有限:只能在协程函数内挂起,无法在任意函数调用深度暂停。
    • 语言依赖:需要语言支持(如 C++20 的 co_await 关键字)。
    • 复杂实现:用户代码简单,但编译器生成的状态机可能难以调试。

有栈 vs 无栈对比

  • 有栈协程适合需要深度调用栈的场景(如复杂递归),但内存占用高。
  • 无栈协程适合高并发、异步 I/O 场景,内存效率高,但受限于语言支持。

2.3 独立栈 vs 共享栈

独立栈(Separate Stack)

  • 定义:每个协程拥有独立的栈空间,互不干扰。
  • 机制:每个协程分配固定或动态大小的栈,上下文切换时保存/恢复栈。
  • 示例:Boost.Coroutine、ucontext(Linux)。
  • 优点
    • 隔离性强:协程间栈独立,互不影响,适合复杂任务。
    • 支持深递归:栈大小可动态调整,适应复杂调用链。
  • 缺点
    • 内存开销大:每个协程需要 KB 级别的栈空间。
    • 切换开销:栈复制或切换成本较高。
    • 资源管理复杂:需要显式分配和回收栈。

共享栈(Shared Stack)

  • 定义:多个协程共享一个栈空间,挂起时保存上下文,恢复时重用栈。
  • 机制:通过状态机或最小上下文保存,栈空间在协程间复用。
  • 示例:C++20 协程(通过编译器优化实现共享栈效果)。
  • 优点
    • 内存效率高:无需为每个协程分配独立栈,适合高并发。
    • 切换快速:上下文切换仅涉及状态恢复,无需复制栈。
  • 缺点
    • 实现复杂:需要编译器或运行时支持栈管理。
    • 限制较多:不支持深递归或复杂调用链,挂起点受限。
    • 调试困难:共享栈可能导致状态管理复杂化。

独立栈 vs 共享栈对比

  • 独立栈适合需要隔离和复杂控制流的场景,但内存和性能开销大。
  • 共享栈适合高并发、轻量级任务,内存效率高,但灵活性较低。

3. C++ 中的协程支持

C++20 引入了协程支持(co_awaitco_yieldco_return),主要实现非对称、无栈、共享栈协程。以下是一个简单的 C++20 协程示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <coroutine>
#include <iostream>

struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};

Task coroutine_example() {
std::cout << "Start coroutine\n";
co_await std::suspend_always{}; // 挂起
std::cout << "Resume coroutine\n";
}

int main() {
auto task = coroutine_example();
std::cout << "Main: before resume\n";
task.handle.resume(); // 恢复协程
std::cout << "Main: after resume\n";
return 0;
}

输出

1
2
3
4
Start coroutine
Main: before resume
Resume coroutine
Main: after resume

说明

  • C++20 协程是非对称的(只能挂起回调用者)。
  • 无栈实现:编译器将协程转换为状态机,上下文存储在堆上。
  • 共享栈:协程不维护独立栈,状态机共享调用者的栈空间。

4. 总结与对比

分类 优点 缺点 适用场景
对称协程 灵活,支持任意协程切换 调度复杂,调试困难 复杂控制流、状态机
非对称协程 简单易用,控制流清晰 灵活性低,依赖调用者 异步 I/O、事件驱动
有栈协程 支持深递归,语言无关 内存开销大,切换慢 复杂任务、深调用链
无栈协程 内存效率高,切换快 挂起点受限,语言依赖 高并发、异步任务
独立栈 隔离性强,支持复杂逻辑 内存和切换开销大 复杂任务、深递归
共享栈 内存效率高,切换快 实现复杂,限制多 高并发、轻量任务

5. 与线程池的联系

结合之前的线程池讨论,协程和线程池可以互补:

  • 线程池:适合 CPU 密集型任务,线程数量受限于核心数。
  • 协程:适合 I/O 密集型任务,可支持百万级并发。
  • 结合方式:在线程池中运行协程,线程处理计算密集任务,协程处理异步 I/O(如 Boost.Asio 结合协程)。

如需 C++ 协程的更详细实现(如与线程池结合、异步 I/O 示例)或特定分类的深入分析,请提供进一步要求!