注:该文用于个人学习记录和知识交流,如有不足,欢迎指点。
一、同步
同步:阻塞时仍等待,等IO执行完之后再去干其他事情
核心:操作的发起和结果处理在同一个执行流中顺序进行
优点:逻辑简单,符合人脑的线性思维
缺点:当IO等待时间过长时,影响性能
cpp
int main
{
read();
// 阻塞等待
parse_data(); // 处理数据
write(); // 存储数据
other(); // 其他操作
}
二、异步
异步:阻塞时不等待,去干别的事情,等IO执行完之后再返回来去执行(非阻塞模型)
核心:"非阻塞的操作执行与结果通知机制"
优点:性能优越
缺点:传统方式的异步实现大都依赖回调、业务复杂时容易回调地狱,同时线程数量有限制(Linux中1线程占8MB),不适合高并发
Linux中提供异步io函数 (aio库):
以aio_read()为例子:
-
非阻塞的操作执行:文件的读取由内核直接调度 I/O 操作,无需用户态线程(即不会阻塞)
-
结果通知机制:读取完毕后,通过信号(需循环判断是否执行完毕)或回调通知(无需判断是否执行完毕,设置回调函数,系统会单开线程执行该回调函数)。
cpp
handle()
{
parse_data(buf);
write(buf);
}
int main()
{
aio_read(fd, buf, handle); //不阻塞等待
other(); // 其他操作
}
三、同步和异步的对比
维度 | 同步 | 异步 |
---|---|---|
执行流 | 阻塞等待,单执行流 | 非阻塞,多执行流 / 事件驱动 |
结果处理 | 立即在当前执行流中获取 | 后续通过回调、事件等异步通知 |
适用场景 | 逻辑简单、操作耗时短的场景 | 高并发、耗时操作(I/O、网络) |
四、协程
核心:同步的编程方式,异步的性能
本质:轻量级线程(用户态实现的CPU调度),形式表现为一个可以暂停和恢复的函数。
遇到阻塞(wait、sleep)时,让出CPU给调度器,调度器分配给非阻塞的协程(ready),常搭配epoll使用
1. 协程有四种状态:
需要CPU:
ready:协程处于非阻塞状态
不需要CPU:
wait:等待IO,IO完毕时,回复ready
sleep: 协程主动休眠(内部调用sleep),超过休眠时间,变回ready
正在使用CPU:
running: 协程执行中
2. 原语操作:
create:协程创建,进入ready
yield: 协程:ready -> wait or sleep 。 让出CPU给调度器
resume: 协程: ready -> running。 调度器将CPU分配给协程
exit:摧毁协程
3. 优势:
- 避免回调地狱:用同步式的代码结构实现异步逻辑,可读性远高于传统异步(如回调、Promise)。
- 轻量级调度:协程的创建和切换开销极低(比线程小几个数量级),支持大规模并发(如百万级协程)。
- 资源高效:无需为每个任务创建线程,减少内存占用和上下文切换开销。
4. 与传统同步、异步的对比
特性 | 传统同步(线程) | 传统异步(回调 / 事件) | 协程 |
---|---|---|---|
编码复杂度 | 低(线性逻辑) | 高(回调嵌套 / 事件注册) | 低(线性逻辑) |
性能开销 | 高(线程切换) | 低(无阻塞) | 极低(用户态调度) |
并发规模 | 有限(线程数受限) | 高(无阻塞) | 极高(百万级协程) |
5. 优缺点
维度 | 详细说明 |
---|---|
优点 | 1. 极致轻量,资源消耗低协程在用户态创建和切换,无需操作系统内核介入,内存占用极小(通常几 KB 栈空间),可创建数十万甚至数百万个协程(而线程通常只能创建数千个,受内核限制)。 2. 调度灵活,无内核切换开销 协程的切换由程序显式控制(如通过yield 、await 或swapcontext ),无需陷入内核态,切换成本仅为保存 / 恢复少量寄存器和栈指针(微秒级,线程切换通常是毫秒级),适合高频切换场景。 3. 避免多线程锁竞争协程通常运行在单线程内("单线程多协程" 模型),共享数据无需加锁(同一时间只有一个协程执行),减少了锁带来的复杂性和性能损耗。 4. 天然支持 "暂停 - 恢复" 模式协程可在执行中主动暂停(如等待 I/O 时),保存当前状态,后续再从暂停点恢复,适合处理需要 "等待 - 继续" 的逻辑(如网络请求、文件读写)。 |
缺点 | 1. 无法直接利用多核 CPU单线程内的协程只能在一个 CPU 核心上运行,若要利用多核,需配合多进程(每个进程运行多个协程),增加了设计复杂度。 2. 依赖语言 / 库支持,调试难度高 协程需要语言原生支持(如 Python 的async/await 、Go 的goroutine )或第三方库(如 C++ 的libco ),不同实现的语法和机制差异大;且执行流是非线性的(频繁暂停 / 恢复),调试时难以跟踪调用栈。 3. 需手动处理阻塞操作 若协程中存在阻塞操作(如同步 I/O)且未主动切换,会阻塞整个线程内的所有协程("一损俱损"),需配合异步 I/O(如epoll 、kqueue )才能发挥优势。 4. 不适合 CPU 密集型任务协程在 CPU 密集型任务中无法并行计算(单线程限制),性能不如多线程(可利用多核)。 |
6. 应用场景
场景类型 | 具体说明 | 示例 |
---|---|---|
I/O 密集型任务 | 适合包含大量等待时间(如网络请求、文件读写、数据库操作)的场景。协程在等待 I/O 时可切换到其他任务,避免 CPU 空闲,大幅提升并发效率。 | - 网络爬虫:同时发起数千个 HTTP 请求,等待响应时切换到其他请求。- Web 服务器:处理数万并发连接,每个连接用协程管理,等待数据时不阻塞。 |
异步编程框架 | 作为异步编程的底层执行单元,简化异步逻辑的编写(无需回调嵌套 "回调地狱")。 | - Python 的asyncio :用协程实现异步网络操作,代码线性编写,可读性高。- Node.js 的async/await (基于协程思想):替代回调函数,简化异步逻辑。 |
游戏开发 | 游戏中角色 AI、动画帧、事件触发等逻辑需要 "暂停 - 恢复"(如技能冷却、对话等待),协程可自然表达这种时序逻辑。 | - 游戏角色释放技能:协程先执行施法动画(1 秒),暂停后等待冷却(5 秒),再恢复执行后续逻辑。 |
数据流 / 管道处理 | 多步骤的数据处理流程(如解析→过滤→转换→输出),每个步骤用协程实现,数据传递时切换执行,提高流水线效率。 | - 日志处理系统:用协程分别处理日志读取、格式解析、关键词过滤,数据流式传递,步骤间无缝切换。 |
嵌入式 / 资源受限场景 | 嵌入式设备内存和 CPU 资源有限,无法创建大量线程,协程的轻量级特性适合实现多任务调度。 | - 物联网设备:用协程同时处理传感器数据采集、网络上报、本地交互,低资源消耗下实现多任务。 |
总结
协程的核心价值是用极低的开销实现高并发,尤其适合 I/O 密集型、需要频繁切换且逻辑存在 "等待 - 恢复" 的场景。但它无法直接利用多核,需配合异步 I/O 和多进程才能发挥最大优势,且不适合 CPU 密集型任务。现代编程语言(如 Go、Python、C++20)已普遍支持协程,使其成为高并发场景的重要解决方案。
五、用户态实现上下文切换的方式(CPU的调度):
协程是一个函数,所谓的CPU调度,可以简单的理解为如何在一个函数执行到一半时跳到另一个函数,同时保存当前函数的执行状态。
实现方式
1. setjmp.h
名称 | 类型 / 性质 | 参数 | 核心作用 | 返回值 / 特殊说明 |
---|---|---|---|---|
jmp_buf |
结构体(特殊类型) | 无(自身为保存载体) | 保存程序的 "执行环境",包括程序计数器、栈指针、寄存器状态等,即当前执行位置和上下文。 | 无(仅作为setjmp 和longjmp 的操作对象) |
setjmp |
函数 | jmp_buf env |
将当前执行环境保存到env 中,作为后续longjmp 的 "跳转目标点"。 |
首次调用返回0 ;被longjmp 跳回时,返回longjmp 指定的非 0 值(用于区分首次执行与跳转)。 |
longjmp |
函数 | jmp_buf env 、int val |
跳转到setjmp 保存的env 环境,恢复当时的执行状态(回到setjmp 调用位置)。 |
无返回值;val 必须为非 0 值,否则行为未定义(val 会作为setjmp 的返回值)。 |
使用方式:
jmp_buf env; // 定义保存当前状态的变量
setjump(env) // 设定锚点
longjump(env,eventi_d) // 跳回到锚点,返回event_id (不能为0),锚点根据event_id 的值执行下一步操作
注意:env必须保证longjump时有效。简单点可以定义为全局变量,当然也可以定义为局部变量作为参数传给需要使用的函数(需确保主函数参数未被销毁)。
基本格式(伪代码):
cpp
jmp_buf env;
void fun()
{
....
longjmp(env,eventid);
}
int main()
{
int ret = set_jump(env) // 初始返回0
if (ret == 0)
{
fun();
}
else if (ret == eventid)
{
....
}
}
1. 例子:
1. env作为参数传入
cpp
#include <stdio.h>
#include <setjmp.h>
void fun(jmp_buf env)
{
printf("come to fun\n");
longjmp(env, 1);
}
int main()
{
jmp_buf env;
int ret = setjmp(env);
if (ret == 0)
{
fun(env);
}
else
{
printf("come back main\n");
}
}
易错:如果fun中也设置了锚点env1,那么env1是不会对env进行保存的,也就是说下一次main跳转到env1的话,fun是使用不了env的!!!!
错误示例:
cpp
#include <stdio.h>
#include <setjmp.h>
jmp_buf env1;
void fun(jmp_buf env)
{
int ret = setjmp(env1);
if (ret == 0)
{
printf("come to fun firstly\n");
longjmp(env, 1);
}
else
{
printf("come to fun secondly\n");
longjmp(env, 1);
}
}
int main()
{
jmp_buf env;
int ret = setjmp(env);
if (ret == 0)
{
fun(env);
}
else if (ret == 1)
{
printf("come back main firstly\n");
longjmp(env1, 1);
}
else
{
printf("come back to main secondly\n");
}
}

fun返回不去main,因为env没有被保存。所以说,稳妥一点,把env变量都定义在全局变量当中。
2. 多个env(锚点)、通过main来分配CPU
cpp
#include <stdio.h>
#include <setjmp.h>
jmp_buf env0; // 锚定 main 中的点
jmp_buf env1; // 锚定 func1 中的点
jmp_buf env2; // 锚定 func2 中的点
enum SIG
{
FUNC1_ONE = 100,
FUNC1_TWO,
FUNC2_ONE,
FUNC2_TWO,
};
void func2()
{
int ret = setjmp(env2);
if (ret == 0)
{
printf("好");
longjmp(env0, FUNC2_ONE);
}
else
{
printf("界");
longjmp(env0, FUNC2_TWO);
}
}
void func1()
{
int ret = setjmp(env1);
if (ret == 0)
{
printf("你");
longjmp(env0, FUNC1_ONE);
}
else
{
printf("世");
longjmp(env0, FUNC1_TWO);
}
}
int main()
{
int ret = setjmp(env0);
switch (ret)
{
case 0:
{
func1();
break;
}
case FUNC1_ONE:
{
func2();
break;
}
case FUNC1_TWO:
{
longjmp(env2, 2);
break;
}
case FUNC2_ONE:
{
longjmp(env1, 2);
break;
}
case FUNC2_TWO:
{
printf("!\n");
break;
}
default:
break;
}
return 0;
}

运行结果如上,通过eventid实现了CPU的定向分配
2. 优缺点、应用场景:
维度 | 内容 |
---|---|
优点 | - 提供非局部跳转能力,可跨多层函数直接跳转,简化深层错误处理流程 - 实现轻量级控制流切换 (如协程、状态机),无需操作系统级线程开销 - 错误处理时避免多层函数返回的繁琐,直接跳回顶层处理逻辑 |
缺点 | - 严重破坏程序控制流和栈结构,代码逻辑难以理解和维护 - 易引发资源泄漏(如局部变量未销毁、文件句柄未关闭等) - 存在未定义行为风险(如跳回已返回函数的环境,导致栈访问越界) - 调试困难,控制流跳跃使得程序执行轨迹难以跟踪 |
应用场景 | - 底层错误处理框架:C 语言早期用于跨函数错误恢复(如文件操作、网络通信的深层错误直接跳回顶层) - 协程 / 状态机实现:早期无 ucontext 时,用于保存 / 恢复执行上下文,实现简单协程或有限状态机 - 遗留系统维护:一些老旧 C 代码中仍依赖其处理复杂控制流,如嵌入式系统、特定领域的底层工具 |
2. ucontext
ucontext.h
是 C 语言中用于用户态上下文管理 的头文件,提供了一组函数和结构体,用于保存、恢复和切换程序的执行上下文(包括寄存器状态、栈指针、程序计数器等),是实现用户级线程(协程) 或复杂状态机的底层工具。
1. 关键结构体:ucontext_t
用于存储程序的 "执行上下文",包含以下核心信息:
- 程序计数器(下一条要执行的指令地址)
- 栈指针(当前栈的位置)
- 通用寄存器状态
- 信号掩码(阻塞的信号集合)
- 关联的栈内存(
uc_stack
,用于上下文切换时的栈空间,需要用户配置) - 后继上下文(
uc_link
,当前上下文结束后自动切换到的上下文,需要用户配置)
2. 核心函数
函数原型 | 作用 |
---|---|
int getcontext(ucontext_t *ucp); |
保存当前执行上下文到 ucp 中(成功返回 0,失败返回 -1)。 |
void setcontext(const ucontext_t *ucp); |
恢复 ucp 中保存的上下文,程序从该上下文的状态继续执行(不会返回,除非上下文切换失败)。 |
void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...); |
修改已初始化的 ucp 上下文:指定新的入口函数 func 、参数,以及栈空间(需先通过 getcontext 初始化并设置 uc_stack )。 |
int swapcontext(ucontext_t *oucp, const ucontext_t *ucp); |
保存当前上下文到 oucp ,同时切换到 ucp 上下文执行(成功返回 0,失败返回 -1)。 作用等价于: -先调用 getcontext(oucp) (保存当前上下文到 oucp ); -再调用 setcontext(ucp) (恢复 ucp 上下文并执行)。 |
流程:
ucontext_t main_ctx, coro_ctx;
getcontext(&main_ctx);
getcontext(&coro_ctx);
coro_ctx.uc_stack.ss_sp = coro_stack; // 设置协程栈
coro_ctx.uc_stack.ss_size = sizeof(coro_stack);
coro_ctx.uc_link = &main_ctx; // 协程结束后自动切回 main_ctx
makecontext(&coro_ctx, coro_func, 0); // 绑定协程入口函数
swapcontext(&main_ctx, &coro_ctx); // 切换到 coro_ctx
注意:必须先调用 getcontext
初始化 ucontext_t
,之后才能配置栈、后继上下文,最后通过 makecontext
设置入口函数,这是 ucontext
机制的强制要求:
-
getcontext
提供基础上下文:它会填充ucontext_t
的所有必要字段(如程序计数器、栈指针、寄存器快照等),这些是makecontext
能够正确修改上下文的前提。 -
makecontext
依赖已有上下文结构:它需要基于getcontext
保存的 "原始状态",替换其中的程序计数器(指向新的入口函数func2
)和栈信息(确保切换后使用新栈),否则无法正确创建可执行的上下文。
3. 协程中栈的作用
作用分类 | 具体说明 | 示例 |
---|---|---|
存储局部变量和临时数据 | 函数执行时的局部变量(如int a=10 、char buf[100] )及运算临时数据,均存储在栈上,避免与其他上下文数据冲突。 |
协程函数coro_func 中printf 调用产生的临时数据,存储在协程专属栈coro_stack 中,不干扰主程序栈。 |
保存函数调用链信息 | 上下文执行中嵌套调用函数时,栈会记录每次调用的返回地址、参数列表等,形成 "调用链",确保函数执行后能正确返回上层。 | 若coro_func 调用sub_func ,sub_func 再调用sub_sub_func ,三次调用的返回地址依次压入协程栈,保证最终能回到coro_func 继续执行。 |
支持上下文独立执行 | 不同上下文(如主程序、协程)作为独立执行流,需各自的栈存储自身函数状态,避免共用栈导致数据覆盖。 | 主程序栈存储main 及相关函数状态,协程栈coro_stack 存储coro_func 及相关函数状态,二者互不干扰。 |
确保切换后正确恢复状态 | 上下文切换时,通过恢复栈指针(指向该上下文上次执行的栈位置),可读取之前的局部变量和调用链信息,继续未完成的逻辑。 | 协程切换回主程序后,主程序从自身栈中读取临时数据,继续执行printf("主程序:从协程返回\n") 。 |
总结:栈是上下文的 "专属内存空间",通过存储临时状态(局部变量、调用链等),保障多个独立执行流(如主程序与协程)的切换互不干扰且能正确恢复,是上下文切换的核心保障。
栈大小选哟"按需分配":
- 先根据场景估算(参考经验值),再通过测试验证(逐步调整至稳定运行)。
- 简单场景:16KB~64KB;中等场景:128KB~512KB;复杂场景:1MB~8MB。
- 优先保证不溢出,再优化内存占用。
4. main_ctx需要分配栈吗?
当然需要,只不过是由操作系统自动分配的
上下文 | 栈来源 | 是否需要手动分配 / 设置 |
---|---|---|
coro_ctx |
用户手动定义的栈(如 coro_stack ) |
是(需设置 uc_stack.ss_sp 和 ss_size ) |
main_ctx |
操作系统自动分配的进程主栈 | 否(getcontext 会自动记录主栈信息) |
5. 简单示例:
cpp
#include <stdio.h>
#include <ucontext.h>
#include <stdlib.h>
ucontext_t main_ctx, coro_ctx;
char coro_stack[1024 * 16]; // 协程栈
// 协程函数
void coro_func() {
printf("协程:开始执行\n");
printf("协程:切换回主程序\n");
swapcontext(&coro_ctx, &main_ctx); // 切换到 main_ctx
printf("协程:再次执行\n");
}
int main() {
// 初始化主程序上下文
getcontext(&main_ctx);
// 初始化协程上下文
getcontext(&coro_ctx);
coro_ctx.uc_stack.ss_sp = coro_stack; // 设置协程栈
coro_ctx.uc_stack.ss_size = sizeof(coro_stack);
coro_ctx.uc_link = &main_ctx; // 协程结束后自动切回 main_ctx
makecontext(&coro_ctx, coro_func, 0); // 绑定协程入口函数
printf("主程序:切换到协程\n");
swapcontext(&main_ctx, &coro_ctx); // 切换到 coro_ctx
printf("主程序:从协程返回\n");
swapcontext(&main_ctx, &coro_ctx); // 再次切换到协程
printf("主程序:结束\n");
return 0;
}

6. 优缺点、应用场景:
维度 | 详细说明 |
---|---|
优点 | 1. 用户态轻量切换:上下文切换在用户态完成,无需陷入内核,开销远低于操作系统线程切换(无内核态 / 用户态切换成本),适合高频切换场景。 2. 完整上下文管理 :不仅保存寄存器和程序计数器,还显式管理栈空间(uc_stack ),支持独立执行流(如协程)的完整生命周期(创建、切换、销毁),比 setjmp/longjmp 更灵活。 3. 控制流灵活:可实现复杂的非局部跳转(如跨多层函数切换),支持 "暂停 - 恢复" 模式,适合状态机、协程等需要保存中间状态的场景。 |
缺点 | 1. 兼容性差:在 POSIX.1-2008 中被标记为 "过时"(obsolescent),部分现代系统(如 macOS)不支持,依赖它的代码可移植性低。 2. 栈管理复杂 :需手动分配和管理栈空间(uc_stack ),需预估栈大小(过小导致溢出,过大浪费内存),且栈溢出无安全保护(直接引发段错误)。 3. 调试困难 :上下文切换会打乱线性执行流,调试时难以跟踪调用栈和执行轨迹,增加问题定位难度。4. API 底层且繁琐 :需按固定流程调用 getcontext →配置栈→makecontext →swapcontext ,使用门槛高,易因步骤错误导致未定义行为(如未初始化上下文直接切换)。 |
应用场景 | 1. 用户级协程 / 轻量级线程 :作为早期协程库的底层实现(如 Python 早期 greenlet ),通过 ucontext 实现多协程间的快速切换,适合 I/O 密集型任务(如网络爬虫、服务器)。 2. 复杂状态机 :在解析器(如语法解析、协议解析)或事件驱动程序中,用 ucontext 保存不同状态的执行上下文,实现状态间的高效切换。 3. 嵌入式系统轻量调度 :资源受限的嵌入式环境中,用 ucontext 实现用户态任务调度(无需操作系统内核支持),降低内存和 CPU 开销。 4. 遗留系统维护 :一些老旧 C 代码(如特定领域工具、早期服务器框架)仍依赖 ucontext 实现控制流,需基于它进行维护或迁移。 |
总结
ucontext
是用户态上下文管理的经典工具,核心价值在于轻量级切换和完整上下文控制 ,但受限于兼容性和使用复杂度,现代开发中已逐渐被语言原生协程(如 C++20 coroutine
)或更高层库替代。其主要适用场景集中在对性能敏感的轻量级并发、复杂状态机,或依赖旧接口的遗留系统。
六、上下文切换是什么
上下文切换 指的是暂停当前程序的执行状态,保存其 "上下文"(执行时的全部状态信息),然后恢复另一个程序的上下文,使其继续执行的过程。简单来说,就是在多个独立的执行流之间 "切换工作",让它们轮流使用 CPU,从而实现并发或多任务的效果。
Tips:"上下文" 是程序执行时的完整状态快照,包含:
- 寄存器值(如通用寄存器、程序计数器 PC,记录下一条要执行的指令地址);
- 栈指针(记录当前栈的位置,用于存储局部变量、函数调用信息);
- 内存中的临时数据(如局部变量、函数参数);
- 程序的执行位置(当前执行到哪一行代码)。
1. 为什么需要上下文切换?
其核心目的是实现 "并发" 或 "多任务",让多个执行流(如进程、线程、协程)看起来 "同时" 执行,提升 CPU 利用率:
- 操作系统层面:比如同时运行浏览器和音乐播放器,操作系统会在它们之间切换,让 CPU 轮流为两个程序服务。
- 用户态层面:比如在一个程序中实现多个协程(轻量级线程),通过上下文切换让协程轮流执行,模拟并发效果。
2. 上下文切换的过程(以 "协程切换" 为例)
以用户态协程为例,切换步骤如下:
- 保存当前上下文 :当需要切换时,先把当前协程的寄存器值、栈指针、程序计数器等状态保存到内存中 (比如
ucontext_t
结构体)。 - 恢复目标上下文 :从内存中读取另一个协程的上下文,恢复其寄存器、栈指针、程序计数器,让 CPU 从该协程的 "暂停点" 继续执行。
3. 不同层面的上下文切换区别
场景 | 切换主体 | 切换位置 | 开销大小 | 典型例子 |
---|---|---|---|---|
操作系统级 | 进程 / 线程 | 内核态 | 大(需陷入内核) | 进程切换、线程调度 |
用户级 | 协程、用户线程 | 用户态 | 小(用户态完成) | 协程切换(如 ucontext) |
4. 通俗类比
可以把 "上下文切换" 想象成:
- 你正在写论文(程序 A),突然要去接电话(程序 B)。
- 你先保存论文的 "当前状态"(比如记下笔停在第几页、当前思路),然后去接电话。
- 接完电话后,恢复论文的状态(回到之前的页数、继续之前的思路),继续写论文。
5. 总结
上下文切换是实现 "多任务并发" 的核心机制,通过保存和恢复程序的执行状态,让多个执行流轮流使用 CPU。它在操作系统(进程 / 线程调度)、用户态编程(协程、状态机)中广泛应用,是现代计算机 "并发能力" 的基础。