进程、线程和协程的区别是什么?

#计算机网络#操作系统#面试八股

进程、线程和协程的区别是什么?

三者是同一条执行流在不同"资源与调度"边界上的切分:进程是资源分配的最小单位,线程是 CPU 调度的最小单位, 协程是用户态协作式调度的最小单位。进程有独立地址空间,线程共享进程地址空间,协程共享线程的栈之外的几乎所有东西, 只保留自己的执行上下文。

1. 一张表看清差别

维度进程 Process线程 Thread协程 Coroutine
资源归属独立地址空间、文件表、信号处理共享所属进程资源共享所属线程/进程资源
私有部分全部栈、寄存器、TID栈、寄存器(很小的栈)
调度者内核调度器内核调度器用户态运行时(或语言 VM)
切换成本高(换页表、刷 TLB)中(进内核,约 2~5us)低(用户态,约百 ns)
创建成本重,fork 要复制页表中,默认栈 MB 级轻,初始栈 KB 级
通信方式IPC(管道/共享内存/socket)共享变量 + 锁共享变量 + 锁/通道
崩溃影响只死自己整个进程挂掉整个进程挂掉
典型数量级几十~几百几百~几千十万~百万

2. 地址空间视角

进程 A                                进程 B
┌───────────────────────────┐        ┌───────────────────────────┐
│ 代码段 / 数据段 / 堆       │        │ 代码段 / 数据段 / 堆       │
│  线程1 栈 │ 线程2 栈 │ ... │        │  线程1 栈 │ ...            │
│  mmap 区 / 页表 / 文件表   │        │  mmap 区 / 页表 / 文件表   │
└───────────────────────────┘        └───────────────────────────┘
       ▲ 内核可见的调度实体:TID           ▲
   协程在用户态,内核根本不知道它的存在:
   G1  G2  G3 ...  ── 由 runtime 复用到少量线程 M 上

关键点:协程没有独立的 TID,内核调度的是线程;协程之间的切换不经过内核, 所以既省了系统调用,也省了 TLB 与缓存污染。

3. 代码与观测

// 10 万个协程,初始栈各 2KB 左右,总量可控
var wg sync.WaitGroup
for i := 0; i < 100000; i++ {
    wg.Add(1)
    go func(n int) {          // go 语句创建一个 G,由 runtime 调度到 M 上执行
        defer wg.Done()
        _ = n * n
    }(i)
}
wg.Wait()
fmt.Println(runtime.NumGoroutine())
# 一个 Go 进程实际有几个内核线程
ps -o pid,nlwp,cmd -p $(pgrep myserver)
# 线程的栈大小(默认 8MB 虚拟地址,按需提交)
ulimit -s
# 上下文切换成本
perf bench sched pipe

4. 常见追问

Q:线程切换为什么比协程贵? 线程切换要陷入内核:保存/恢复全部寄存器、切换内核栈、可能换页表导致 TLB 失效、走调度器选下一个任务。 协程切换只是保存少量寄存器到用户态结构体、换一下栈指针,一次几十到几百纳秒,且不污染 TLB。

Q:进程切换比线程切换贵在哪? 主要贵在地址空间:切换 CR3、刷新 TLB(有 PCID/ASID 可缓解)、换页表后缓存局部性变差。 线程共享页表,所以便宜得多。

Q:有哪几种线程模型? 一对一线程(1:1,Linux pthread/Java Thread,简单但受内核线程数限制)、 用户态多对一(N:1,早期绿色线程,一个阻塞全阻塞)、 混合 M:N(Go 的 GMP、Erlang 的进程),兼顾数量与并行。

Q:协程会不会"饿死"其他协程? 会。协作式调度依赖协程主动让出(IO、channel、runtime.Gosched),如果某个协程长时间纯计算不阻塞, 同线程上的其他协程就得等。Go 1.14 之后引入基于信号的异步抢占,缓解了这个问题。

Q:多进程和多线程怎么选? 需要强隔离、崩溃不影响他人、能利用多机扩展 → 多进程(Nginx worker、Chrome 标签页); 需要共享大量内存、创建频繁、通信简单 → 多线程;海量并发 IO → 协程。

小结

进程管资源,线程管调度,协程管并发度。切换成本从高到低是进程 > 线程 > 协程, 隔离性也是同样的顺序。回答时先给这张三层表,再补一句"协程由 runtime 调度到线程上, 内核只看见线程",最后用 GMP 或 1:1 模型收尾,逻辑就闭环了。

笔记加载中…