☰
进程、线程、协程有什么区别?从资源分配到调度,一篇理清
2026/10/15 4:01:46 网站建设 项目流程

一个进程里开两个线程去改同一个数组,一个线程写进去的值,另一个线程立刻就能读出来;把同样的逻辑改成两个进程,就读不到了,得改用共享内存之类的东西才能互相看到。这个差别不是语言给的,是操作系统在这两个东西身上分配的资源不一样。

进程、线程、协程经常被摆在一起比,但它们其实站在三个层次上:进程是分配资源的单位,线程是CPU 调度的单位,协程是用户态里自己调度的单位。下面按这三个层次来拆。


资源分配

先看一样东西,在 Linux 上跑ps -eLf:

UID PID PPID LWP C NLWP CMD wuwu 100 99 100 0 3 ./demo wuwu 100 99 101 0 3 ./demo wuwu 100 99 102 0 3 ./demo

(中间省略了STIME、TTY、TIME等列)

同一个程序三行,PID都是 100,LWP(轻量级进程,就是线程 ID) 分别是 100、101、102。也就是说Linux 内核里面,进程和线程并不是两种不同的东西,区别只在于"共享不共享资源"。

每个进程真正独占的,是这几样:

资源进程内各线程
虚拟地址空间(页表)共享同一份
文件描述符表共享
信号处理表共享
当前工作目录、umask共享
栈每个线程一份
寄存器(PC、SP)每个线程一份
线程本地存储 TLS每个线程一份

"共享什么"并不是写死的。fork()和pthread_create()底层是同一个系统调用clone(),只是传的 flag 不同:

flag效果
CLONE_VM父子共享地址空间(页表)
CLONE_FILES共享文件描述符表
CLONE_FS共享 cwd、根目录、umask
CLONE_SIGHAND共享信号处理表
CLONE_THREAD放进同一个线程组,PID 相同

fork()一个 flag 都不传,所以是完整的拷贝(地址空间靠写时复制 COW,先共享物理页,写的时候才复制);pthread_create()会带上CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD这几个(另外还有几个管理用的),于是新东西和调用者共享地址空间和一大堆表,只独立出栈和寄存器——这就是线程。

所以"线程是轻量级进程"这个说法不是比喻。内核里它们就是同一套task_struct,区别全在clone的那几个 flag 上。


调度

地址空间是"分配给谁"的问题,调度是"CPU 现在跑谁"的问题。

Linux 的调度器 CFS 调度的单位是task_struct,它不区分进程和线程,一律按线程调度。一个进程哪怕只有一个线程,在调度器眼里也就是一个 task。/proc/<pid>/task/下面每个线程一个目录,也能看出内核是按线程记账的。

区别在这里:一次线程切换,CPU 换一下栈指针和一组寄存器就完了,因为大家用的是同一张页表;一次进程切换,除了换寄存器,还得换页表基址寄存器(CR3),这一步会牵连到 TLB。


切换开销

页表一换,TLB(Translation Lookaside Buffer,缓存"虚拟页→物理页"映射的那张快表)里的映射就全不对了,因为它们是按上一个进程的虚拟地址存的。于是切换后的一段时间里,访问内存几乎每次都 TLB miss,要重新走一遍页表(可能得访存好几次),程序莫名其妙就慢了。这叫TLB 抖动。

缓解手段是PCID(Process Context IDentifier):给 TLB 每一项打个进程标签,切进程时不整个清空,只是别人的项不再命中。但同一个物理页对应的 CPU 缓存(L1/L2)该污染的还是会污染。

切换类型大致开销主要成本
进程切换几微秒换 CR3 + TLB 失效 + 保存恢复寄存器/栈
线程切换亚微秒只换寄存器/栈,不换页表
协程切换几十纳秒用户态换少量寄存器,不陷入内核

这些数字是量级,具体和 CPU、缓存命中情况都有关系。差距的来源就是上面两行:要不要换页表,要不要进内核。


协程

前面两种切换都要陷入内核(从用户态进内核态,下一篇会专门讲),而这一步本身就贵。协程的思路是:既然贵在内核掺和,那干脆不进内核,在用户态自己切。

协程的调度器在用户态(Go 的 runtime、Python 的 asyncio 事件循环、Lua 的 coroutine),它自己维护一堆待跑的协程,谁该跑了由它决定,切换就是保存/恢复几个寄存器、换一下用户栈,全程不碰内核。

两个直接结果:

  • 栈可以很小。内核线程的栈受ulimit -s限制,Linux 默认 8MB 一个,开几千个就没了;Go 的 goroutine 初始栈只有几 KB,还能按需增长,所以一个进程里跑几十万个协程是常态。
  • 切换是"协作式"的。协程不会在任意时刻被抢走,它得自己主动让出(执行到await、yield,或者 Go 里运行时发现它要阻塞 IO 了),没有抢占也就没有随时可能到来的中断。

代价是协程只是用户态的抽象,干活的内核线程数没变。某一刻真正并行执行的协程,上限就是底层内核线程数(Go 里由GOMAXPROCS控制),协程再多也是它们轮流跑。


三者对比

进程线程协程
资源分配独立的地址空间、fd 表、信号表共享所属进程的这些共享所属线程的一切
谁调度内核内核用户态 runtime
切换开销微秒级,换页表 + TLB 失效亚微秒,不换页表纳秒级,不进内核
栈独立地址空间独立栈,默认 MB 级独立栈,初始 KB 级
数据交换必须走 IPC直接读写共享内存直接读写
一个崩了只影响自己整个进程一起挂整个进程一起挂
数量级几百到几千几千几十万

所以实际怎么选:需要强隔离的(浏览器给每个标签页一个进程)用进程;需要大量共享数据、频繁通信的用线程;并发数要上万、切换开销成了瓶颈的,再考虑协程。但要注意,协程不解决并行——它省的是切换和内存,不是 CPU。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询