一、基本概念
1. 进程(Process)——“独立的小房子”
-
操作系统分配资源的最小单位:有独立地址空间、文件描述符表、环境等。
-
进程之间相互独立,切换开销大(内核调度 + 内存上下文切换)。
想象每个进程就是一间独立的小房子,有自己的水电、家具、食物(独立的内存空间、文件描述符等)。不同房子之间互不干扰,搬东西要经过院子(系统调用) → 开销大。
2. 线程(Thread)——“同一房子里的不同人”
-
线程是进程内的执行流,一个进程可以有多个线程。
-
线程共享进程的内存和资源,但每个线程有自己的栈和寄存器上下文。
-
线程切换通常由内核调度(抢占式),代价比同进程内的函数调用大,但比进程切换小。
一个房子里可以住好几个人,每个人有自己的床(栈)和行李(寄存器上下文),但大家共用厨房、客厅(共享进程内存)。人之间可以互相影响,但搬东西或切换注意力时比换房子快很多(线程切换比进程切换轻)。
3. 协程(Coroutine)——“同一个人多任务切换”
-
协程是一种用户态的轻量级并发,也可理解为“比线程更轻”的执行单元。
-
关键特征:由用户程序/运行时自行调度(通常是协作式),切换开销小;一个线程可以挂起并运行多个协程。
-
协程不是线程,不会自动并行(单线程下是并发),但可以被映射到多个线程/核上实现并行。
想象你只有一个人,但可以同时处理多个小任务:一会儿做作业,一会儿煮饭,一会儿看书。你自己决定什么时候切换任务(协作式调度),不需要打扰别人(用户态切换开销小)。
二、“程序如何假装同时做很多事”?
CPU 在任意时刻只能执行一条指令流(单核情况)。
换句话说:
👉 本质上它一次只能干一件事。
但是我们每天看到的电脑:
-
播着音乐;
-
同时你在打字;
-
浏览器还在刷网页。
这就是“假装同时做很多事”。
我们看到“同时做很多事”是因为系统快速切换任务,让人肉眼或应用看来是并发执行。它是怎么做到的呢?
-
内核调度(线程/进程):内核保存寄存器/栈等上下文 → 切换到另一个线程 → 继续执行。
-
用户态调度(协程):程序自己在用户态保存/恢复上下文,切换比内核调度更快,不需要陷入内核态。
把它想象成:电影院每秒放 24 帧,快到我们觉得是连续动作;CPU 切换也快到我们觉得好像同时在执行多件事 —— 因此是“假装同时”。
所以,“假装同时做很多事”其实就是:
👉 程序不断地在不同任务之间 保存和恢复上下文,让人感觉它们在并行。
三、协程:它来自哪里?为什么会出现?
背景动因:
1、传统同步模型的痛点
-
同步 I/O:程序发起一次网络/磁盘请求就必须等待完成才能继续执行。
-
问题:当有成千上万的连接或请求时,每个请求都要占用一个线程或进程 → 开销巨大,资源容易耗尽。
-
总结:同步模型写起来简单直观,但面对高并发时效率低下。
2、传统异步模型的痛点
-
异步 I/O / 回调:发起请求后不阻塞线程,等事件完成再通过回调处理。
-
问题:代码逻辑被拆成很多回调,嵌套层层叠加 → 出现“回调地狱”,可读性差,调试难度大。
-
总结:异步解决了高并发问题,但牺牲了代码的简洁性和可维护性。
3、协程的出现
协程诞生于想同时兼顾高并发和写代码方便的需求:
-
高并发:协程在用户态调度,切换开销很小。一个线程可以挂起/恢复成百上千个协程 → 节省线程资源。
-
同步风格:编程时依然可以写“顺序执行”的代码,不必嵌套大量回调 → 易读易维护。
-
解决问题:协程结合了同步代码的直观性和异步 I/O 的高效性。
四、协程的直观示例
用 ucontext 来实现最简单的协程示例:
#include <stdio.h>
#include <ucontext.h>
ucontext_t ctx_main, ctx_task1, ctx_task2;
void task1() {
for(int i = 0; i < 3; i++) {
printf("Task1 step %d\n", i+1);
swapcontext(&ctx_task1, &ctx_task2); // 切换到 task2
}
}
void task2() {
for(int i = 0; i < 3; i++) {
printf("Task2 step %d\n", i+1);
swapcontext(&ctx_task2, &ctx_task1); // 切换回 task1
}
}
int main() {
char stack1[1024*16], stack2[1024*16];
// 初始化 task1
getcontext(&ctx_task1);
ctx_task1.uc_stack.ss_sp = stack1;
ctx_task1.uc_stack.ss_size = sizeof(stack1);
ctx_task1.uc_link = &ctx_main;
makecontext(&ctx_task1, task1, 0);
// 初始化 task2
getcontext(&ctx_task2);
ctx_task2.uc_stack.ss_sp = stack2;
ctx_task2.uc_stack.ss_size = sizeof(stack2);
ctx_task2.uc_link = &ctx_main;
makecontext(&ctx_task2, task2, 0);
printf("Start coroutines:\n");
swapcontext(&ctx_main, &ctx_task1); // 先进入 task1
printf("All tasks finished.\n");
return 0;
}
-
初始化上下文
getcontext
获取当前上下文(寄存器、栈、程序计数器等),然后用makecontext
绑定要执行的函数。 -
切换上下文
swapcontext(&当前, &目标)
:保存当前上下文,然后切换到目标上下文执行。 -
协程运行效果
输出类似:
Start coroutines:
Task1 step 1
Task2 step 1
Task1 step 2
Task2 step 2
Task1 step 3
Task2 step 3
All tasks finished.
可以看到 task1 和 task2 交替执行,就像“假装同时做很多事”,但实际上是单线程协作切换。
协程的优势体现:
-
轻量级:不用每个任务都开线程,节省内存和调度开销。
-
顺序风格:写代码像同步一样顺序执行,但实现的是异步高效并发。
-
用户态切换:无需内核调度,切换比线程快很多。