引言
Go协程介绍
Go 协程(Goroutine)是 Go 语言实现高并发的核心机制,它不同于传统的操作系统线程(Thread),是一种用户级轻量级线程。每个 Goroutine 的初始栈大小仅为 2KB(可通过运行时调整),且由 Go 运行时(Runtime)直接管理,而非操作系统内核。这种设计使得 Goroutine 的创建、销毁和切换成本极低,轻松支持百万级并发。
没接触过GO或没听说过协程可以看看视频
GO协程的调度策略:
- 协作式调度:Goroutine 主动让出 CPU(如
runtime.Gosched()或通道阻塞)。 - 抢占式调度(Go 1.14+):通过 SIGURG 信号 强制挂起长时间运行的 Goroutine,防止饿死。
GO协程是一种有栈协程
什么是有栈协程?
有栈协程(Stackful Coroutine)是具备独立栈空间的协程,其特点包括:
- 任意嵌套挂起:可在多层函数调用中挂起,恢复时能正确回到调用点。
- 显式/隐式切换:支持开发者手动让出(如
yield)或由运行时自动抢占。 - 典型实现:Go 的 Goroutine、Lua 的协程(配合 LuaJIT)、C++ 的 Boost.Context。
对比无栈协程(Stackless Coroutine,如 Python 生成器):
| 特性 | 有栈协程 | 无栈协程 |
|---|---|---|
| 栈空间 | 独立分配,可动态扩展 | 无独立栈,依赖闭包状态 |
| 挂起点 | 任意函数调用层级 | 仅限顶层生成器函数 |
| 切换开销 | 较高(需保存完整上下文) | 较低(仅保存局部状态) |
| 典型应用场景 | 通用并发模型 | 简单迭代器/状态机 |
这里说的函数栈空间,是指函数栈帧,由RSP,RBP寄存器控制
基于信号的抢占式协程切换原理
有栈协程的切换原理与操作系统线程切换在核心逻辑上高度相似,但实现层级和开销存在本质差异。操作系统线程切换由内核调度器通过硬件中断(如时钟中断)触发,需陷入内核态保存/恢复全套寄存器(通用寄存器、栈指针、指令指针等)、更新内存映射(CR3寄存器)及线程控制块(TCB),这一过程涉及特权级切换和缓存失效,单次开销常达微秒级。有栈协程切换则完全在用户态完成,由协程库(而非内核)直接通过汇编指令保存/恢复关键寄存器(如%rip、%rsp、%rbp)和切换私有栈空间(通过修改栈指针实现),无需特权切换或内存映射更新,单次切换可控制在百纳秒内。两者均通过“保存现场-加载新现场”实现执行流跳转,但协程因规避了内核边界 crossing 和复杂的权限检查,成为更轻量的“用户态线程”——正如线程是进程的轻量化,协程则是线程的进一步抽象,通过牺牲全局抢占性换取极致的切换效率。
简单协程实现
Linux信号处理
Linux 信号是一种异步事件通知机制,用于进程间通信或响应特定系统事件。其本质是内核向进程发送的软中断(编号 1~64),典型场景包括:
- 用户输入:
SIGINT(Ctrl+C)、SIGTSTP(Ctrl+Z)。 - 硬件异常:
SIGSEGV(段错误)、SIGFPE(除零错误)。 - 定时器触发:
SIGALRM(定时器到期)、SIGVTALRM(虚拟时间计时器)。
注意:
信号处理函数执行时,可能中断任意线程的正常执行流,因此禁止调用非异步安全函数(如 malloc、printf)。协程调度器若在信号处理函数中触发切换,需确保所有操作仅涉及寄存器操作或原子变量。
定时器信号
此时实现中,打算使用定时器信号处理来实现定时中断协程效果
#include <iostream>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include "sys/ucontext.h"
#include <sys/time.h>
#include "cstring"
#include "task.h"
void signal_handler(int signum, siginfo_t *info, void *ucontext) {
// 协程切换操作...
}
/**
*
* 设置信号处理函数
*/
void setup_signal_handler() {
struct sigaction sa;
sa.sa_sigaction = signal_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_SIGINFO ; // 使用 SA_SIGINFO 获取上下文
// 注册 SIGALRM 信号处理
if (sigaction(SIGALRM, &sa, NULL) == -1) {
perror("sigaction");
exit(1);
}
// 设置定时器触发信号
struct itimerval timer = {
.it_interval = {.tv_sec = 0, .tv_usec = 100000}, // 100ms
.it_value = {.tv_sec = 0, .tv_usec = 100000}
};
setitimer(ITIMER_REAL, &timer, NULL);
}
CPU寄存器保存与恢复
进入信号处理函数时,参数ucontext保存了CPU进入信号处理函数前的各种寄存器,若我们修改这个寄存器数组里面的数组,Linux在执行完信号处理函数恢复主流程时,把ucontext中的寄存器状态恢复到CPU上,所以我们可以在信号处理函数中保存中断前CPU现场,并恢复另一个协程的CPU现场
#define MAX_COROUTINES 10 // 系统支持的最大协程数量
// 协程状态机定义
typedef enum {
COROUTINE_READY, // 就绪态(可被调度)
COROUTINE_RUNNING, // 运行态(正在执行)
COROUTINE_BLOCKED // 阻塞态(等待资源)
} coroutine_state;
// 协程控制块(Coroutine Control Block)
typedef struct {
ucontext_t context; // 协程上下文(保存寄存器等状态)
char* stack; // 协程栈空间指针(需手动分配)
coroutine_state state; // 当前状态
void (*func)(void*); // 协程入口函数指针
void *arg; // 传递给入口函数的参数
} coroutine_t;
// 调度器结构(全局单例)
typedef struct {
coroutine_t **coroutines = new coroutine_t*[10]; // 协程指针数组(硬编码大小为10)
int num_coroutines = 0; // 当前管理的协程数量
int current_index = 0; // 当前正在运行的协程索引
} scheduler_t;
scheduler_t scheduler; // 全局调度器实例
// 保存寄存器状态到目标上下文
// current: 目标上下文结构
// uc: 当前上下文来源
void save_registers(ucontext_t *current, ucontext_t *uc) {
current->uc_mcontext.gregs[REG_RAX] = uc->uc_mcontext.gregs[REG_RAX];
current->uc_mcontext.gregs[REG_RCX] = uc->uc_mcontext.gregs[REG_RCX];
current->uc_mcontext.gregs[REG_RDX] = uc->uc_mcontext.gregs[REG_RDX];
current->uc_mcontext.gregs[REG_RSI] = uc->uc_mcontext.gregs[REG_RSI];
current->uc_mcontext.gregs[REG_RDI] = uc->uc_mcontext.gregs[REG_RDI];
current->uc_mcontext.gregs[REG_R8] = uc->uc_mcontext.gregs[REG_R8];
current->uc_mcontext.gregs[REG_R9] = uc->uc_mcontext.gregs[REG_R9];
current->uc_mcontext.gregs[REG_R10] = uc->uc_mcontext.gregs[REG_R10];
current->uc_mcontext.gregs[REG_R11] = uc->uc_mcontext.gregs[REG_R11];
current->uc_mcontext.gregs[REG_RBX] = uc->uc_mcontext.gregs[REG_RBX];
current->uc_mcontext.gregs[REG_RBP] = uc->uc_mcontext.gregs[REG_RBP];
current->uc_mcontext.gregs[REG_R12] = uc->uc_mcontext.gregs[REG_R12];
current->uc_mcontext.gregs[REG_R13] = uc->uc_mcontext.gregs[REG_R13];
current->uc_mcontext.gregs[REG_R14] = uc->uc_mcontext.gregs[REG_R14];
current->uc_mcontext.gregs[REG_R15] = uc->uc_mcontext.gregs[REG_R15];
current->uc_mcontext.gregs[REG_RSP] = uc->uc_mcontext.gregs[REG_RSP];
current->uc_mcontext.gregs[REG_RIP] = uc->uc_mcontext.gregs[REG_RIP];
current->uc_mcontext.gregs[REG_EFL] = uc->uc_mcontext.gregs[REG_EFL];
}
// 从上下文恢复寄存器状态
// current: 源上下文
// uc: 目标上下文(即将激活的上下文)
void restore_registers(ucontext_t *current, ucontext_t *uc) {
// 逆向操作,将保存的寄存器值写回新上下文
uc->uc_mcontext.gregs[REG_RAX] = current->uc_mcontext.gregs[REG_RAX];
uc->uc_mcontext.gregs[REG_RCX] = current->uc_mcontext.gregs[REG_RCX];
uc->uc_mcontext.gregs[REG_RDX] = current->uc_mcontext.gregs[REG_RDX];
uc->uc_mcontext.gregs[REG_RSI] = current->uc_mcontext.gregs[REG_RSI];
uc->uc_mcontext.gregs[REG_RDI] = current->uc_mcontext.gregs[REG_RDI];
uc->uc_mcontext.gregs[REG_R8] = current->uc_mcontext.gregs[REG_R8];
uc->uc_mcontext.gregs[REG_R9] = current->uc_mcontext.gregs[REG_R9];
uc->uc_mcontext.gregs[REG_R10] = current->uc_mcontext.gregs[REG_R10];
uc->uc_mcontext.gregs[REG_R11] = current->uc_mcontext.gregs[REG_R11];
uc->uc_mcontext.gregs[REG_RBX] = current->uc_mcontext.gregs[REG_RBX];
uc->uc_mcontext.gregs[REG_RBP] = current->uc_mcontext.gregs[REG_RBP];
uc->uc_mcontext.gregs[REG_R12] = current->uc_mcontext.gregs[REG_R12];
uc->uc_mcontext.gregs[REG_R13] = current->uc_mcontext.gregs[REG_R13];
uc->uc_mcontext.gregs[REG_R14] = current->uc_mcontext.gregs[REG_R14];
uc->uc_mcontext.gregs[REG_R15] = current->uc_mcontext.gregs[REG_R15];
uc->uc_mcontext.gregs[REG_RSP] = current->uc_mcontext.gregs[REG_RSP];
uc->uc_mcontext.gregs[REG_RIP] = current->uc_mcontext.gregs[REG_RIP];
uc->uc_mcontext.gregs[REG_EFL] = current->uc_mcontext.gregs[REG_EFL];
}
// 信号处理函数(驱动协程切换的核心)
// signum: 信号编号(如SIGALRM)
// info: 信号附加信息
// ucontext: 当前线程的上下文信息
void signal_handler(int signum, siginfo_t *info, void *ucontext) {
// 获取当前正在运行的协程
int idx = scheduler.current_index;
coroutine_t *current = scheduler.coroutines[idx];
auto uc = (ucontext_t*)ucontext; // 转换为上下文指针
// 保存当前协程的执行上下文
if (current->state == COROUTINE_RUNNING) {
save_registers(¤t->context, uc); // 寄存器状态保存
current->state = COROUTINE_READY; // 状态转为就绪
}
coroutine_t *next = current; // 默认继续执行当前协程
// 简单轮询调度算法:选择下一个协程(环形队列)
scheduler.current_index = (scheduler.current_index + 1) % scheduler.num_coroutines;
next = scheduler.coroutines[scheduler.current_index];
// 准备切换到下一个协程
current = next; // 更新当前协程指针
// 恢复目标协程的寄存器状态
restore_registers(¤t->context, uc);
// 更新协程状态为运行态
current->state = COROUTINE_RUNNING;
}
协程初始化
初始化有栈协程需要分配栈空间,协程是独立执行流,需要私有栈空间存储:
- 函数调用链(返回地址、栈帧)
- 局部变量
- 寄存器保存区域
没有独立栈会导致多个协程的栈数据互相覆盖,产生不可预知的错误。
RSP/RBP 需要指向 STACK_SIZE - 1因为在 x86_64 架构中,栈是向下增长的,即从高地址向低地址增长
#define STACK_SIZE (1024 * 64) // 协程栈大小
void coroutine_init(void (*func)(void*), void *arg) {
coroutine_t *co = new coroutine_t{}; // 创建协程控制块
co->func = func; // 绑定入口函数
co->arg = arg; // 保存参数指针
co->state = COROUTINE_READY; // 初始状态为就绪态
// 分配协程栈空间(16字节对齐)
if (posix_memalign((void**) &co->stack, 16, STACK_SIZE) != 0) {
throw std::runtime_error("申请内存失败");
}
// 初始化栈指针和入口地址
co->context.uc_mcontext.gregs[REG_RSP] = (long long)co->stack + STACK_SIZE - 1; // 栈指针指向栈顶
co->context.uc_mcontext.gregs[REG_RBP] = (long long)co->stack + STACK_SIZE - 1; // 基址指针初始化
co->context.uc_mcontext.gregs[REG_RIP] = (long long)func; // 指令指针指向入口函数
// 将新协程加入调度器
scheduler.coroutines[scheduler.num_coroutines] = co;
scheduler.num_coroutines++;
}
编写协程函数
编写task 1, 2, 3分别执行不同的计算任务,并且这些计算任务都有递归调用,用来验证协程栈空间是否设置正确。
#include <stdio.h>
#include <unistd.h>
#define COLOR_RESET "\033[0m" // 重置所有属性
#define COLOR_RED "\033[31m" // 红色文本
#define COLOR_GREEN "\033[32m" // 绿色文本
#define COLOR_YELLOW "\033[33m" // 黄色文本
#define COLOR_BLUE "\033[34m" // 蓝色文本
#define COLOR_MAGENTA "\033[35m" // 洋红色文本
#define COLOR_CYAN "\033[36m" // 青色文本
#define COLOR_WHITE "\033[37m" // 白色文本
// 背景颜色
#define BG_RED "\033[41m" // 红色背景
#define BG_GREEN "\033[42m" // 绿色背景
#define BG_YELLOW "\033[43m" // 黄色背景
#define BG_BLUE "\033[44m" // 蓝色背景
#define BG_MAGENTA "\033[45m" // 洋红色背景
#define BG_CYAN "\033[46m" // 青色背景
#define BG_WHITE "\033[47m" // 白色背景
// 递归计算斐波那契数列的第n项
unsigned long long fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// 递归判断一个数是否为质数
int is_prime_helper(int n, int divisor) {
if (divisor * divisor > n) return 1; // 如果除数平方大于n,说明n是质数
if (n % divisor == 0) return 0; // 如果n能被divisor整除,说明n不是质数
return is_prime_helper(n, divisor + 1); // 递归检查下一个除数
}
int is_prime(int n) {
if (n <= 1) return 0;
if (n <= 3) return 1;
if (n % 2 == 0) return 0; // 偶数不是质数
return is_prime_helper(n, 3); // 从3开始递归检查
}
// 计算斐波那契数列的任务(递归实现)
void task1(void*) {
while (1) {
int n = 40; // 计算斐波那契数列的第40项
unsigned long long result = fibonacci(n);
printf(BG_RED "协程一" COLOR_RESET "计算斐波那契数列第%d项: %llu\n", n, result);
usleep(50000);
}
}
// 计算质数的任务(递归实现)
void task2(void*) {
while (1) {
int count = 0;
for (int i = 0; i < 10000; i++) {
if (is_prime(i)) {
count++;
}
}
printf(BG_GREEN "协程二" COLOR_RESET "计算0到10000之间的质数个数: %d\n", count);
usleep(50000);
}
}
// 计算斐波那契数列的任务(与task1不同,递归实现)
void task3(void*) {
while (1) {
int n = 35; // 计算斐波那契数列的第35项
unsigned long long result = fibonacci(n);
printf( BG_CYAN "协程三" COLOR_RESET "计算斐波那契数列第%d项: %llu\n", n, result);
usleep(50000);
}
}
主函数
int main() {
coroutine_init(task1, nullptr);
coroutine_init(task2, nullptr);
coroutine_init(task3, nullptr);
setup_signal_handler();
while (1)
{
}
return 0;
}
运行结果
可以看到3个协程正在交替执行代码
协程二计算0到10000之间的质数个数: 1229
协程二计算0到10000之间的质数个数: 1229
协程三计算斐波那契数列第35项: 9227465
协程二计算0到10000之间的质数个数: 1229
协程二计算0到10000之间的质数个数: 1229
协程三计算斐波那契数列第35项: 9227465
协程二计算0到10000之间的质数个数: 1229
协程二计算0到10000之间的质数个数: 1229
协程三计算斐波那契数列第35项: 9227465
协程二计算0到10000之间的质数个数: 1229
协程二计算0到10000之间的质数个数: 1229
协程三计算斐波那契数列第35项: 9227465
协程二计算0到10000之间的质数个数: 1229
协程二计算0到10000之间的质数个数: 1229
协程三计算斐波那契数列第35项: 9227465
协程一计算斐波那契数列第40项: 102334155
协程二计算0到10000之间的质数个数: 1229
协程二计算0到10000之间的质数个数: 1229
协程三计算斐波那契数列第35项: 9227465
协程二计算0到10000之间的质数个数: 1229
协程二计算0到10000之间的质数个数: 1229