线程:轻量级进程与多线程模型
基于通用操作系统概念 · 核于 2026-08
速查
- 线程(Thread)定义:进程内的执行单元,是 CPU 调度的基本单位(进程降为资源单位)。同进程的多个线程共享地址空间(代码段/数据段/堆/打开文件表/信号处理),但各有独立的栈、PC、寄存器现场、TLS、errno。
- 为什么引入线程:进程切换太贵(换页表/flush TLB)、进程间通信太麻烦(IPC);线程在同一地址空间内切换不换页表(便宜)、共享内存通信直接(方便)。进程是"重隔离重资源",线程是"轻执行高并发"。
- TCB(线程控制块):描述线程的结构(线程 ID、寄存器现场、栈指针、状态、所属进程指针)。同进程的多个 TCB 关联到同一 PCB,共享那份资源。
- 线程的共享与私有:共享——代码段、数据段(全局变量)、堆(malloc 的)、打开文件表、信号处理、地址空间;私有——栈(函数调用)、PC、寄存器现场、栈指针、线程局部存储(TLS)、errno、信号掩码。
- 用户级线程(ULT):由用户态线程库管理(如早期 Java 绿色线程、协程),内核完全感知不到,整个进程被当作一个调度实体。优点:切换极快(不陷内核)、可自定义调度;缺点:①一个线程阻塞会阻塞整个进程;②不能利用多核并行(内核只给进程分一个核)。
- 内核级线程(KLT):内核直接管理,每个线程是独立调度实体(Linux 用
task_struct,线程与进程统一)。优点:能多核并行、一个线程阻塞不影响其它;缺点:创建/切换要陷内核,开销大。 - 多对一模型:多个用户线程映射到一个内核线程。切换快但不能并行、一个阻塞全阻塞(ULT 特性)。代表:早期 Solaris Green Threads。
- 一对一模型:每个用户线程对应一个独立内核线程。能并行、阻塞隔离,是主流(Linux pthread、Windows)。缺点:内核线程有上限、切换开销大。
- 多对多模型:M 个用户线程映射到 N 个内核线程(N≤M),兼顾并行与轻量。代表:Go 的 goroutine(GMP 调度,M 个 goroutine 跑在 N 个 OS 线程上)、Solaris M:N。
- 进程 vs 线程对比(核心四维):①资源——进程独立地址空间(隔离),线程共享地址空间;②切换代价——进程贵(换页表/flush TLB,数 μs),线程便宜(不换页表,亚 μs);③通信——进程要 IPC(管道/消息队列/共享内存),线程直接共享内存(但要同步);④崩溃影响——进程崩溃不影响其它,线程崩溃(踩坏共享内存)拖垮整个进程。
- 多线程优势:响应性(UI 后台线程不卡)、资源共享(共享地址空间通信便宜)、经济(创建/切换比进程便宜)、多核利用(真正并行)。
- 多线程风险:竞态(共享变量需同步)、死锁(锁循环等待)、饥饿(优先级低的线程长时间得不到 CPU)、内存可见性(cache 一致性问题)。
- 进阶顺序:本叶 → 参考(速查大表)→ 并发同步章节(锁/信号量)。
一、为什么有了进程还要有线程
进程设计之初是为了隔离——每个进程独立地址空间,互不干扰。但隔离带来两个代价:①进程切换要换页表、flush TLB,代价高;②进程间通信要走 IPC(管道/消息队列),复杂且慢。而在很多场景(如 Web 服务器同时处理多个请求),我们想要的是"共享资源、并发执行"——这些执行流需要访问同一份内存(如缓存)、同一批文件句柄,又希望切换便宜。于是线程应运而生:
- 线程 = 进程内的执行流:一个进程可以包含多个线程,它们共享进程的全部资源(地址空间、打开文件、信号处理),但各有独立的执行栈与寄存器现场。
- 进程降级为"资源容器":有了线程后,进程是资源分配单位,线程是 CPU 调度单位。CPU 实际调度的是线程,不是进程。
- "轻量级进程":线程之所以"轻",是因为它不携带独立的地址空间与资源(那部分跟进程共享),切换时只换栈和寄存器,不换页表、不 flush TLB——这是线程比进程便宜的根本原因。
二、线程的共享与私有
理解线程,关键是分清"哪些共享、哪些私有"——这直接决定线程何时需要同步、何时崩溃会扩散:
| 类别 | 内容 | 为什么 |
|---|---|---|
| 共享 | 代码段、数据段(全局变量)、堆(malloc/new)、打开文件表、信号处理函数、地址空间 | 这些是进程级资源,所有线程共用一份 |
| 私有 | 栈、PC(执行到哪)、寄存器现场、栈指针、TLS(线程局部存储)、errno、信号掩码 | 每个线程有独立的执行轨迹,需要独立的调用栈 |
- 共享带来的两面:好处是线程间通信极方便(直接读写全局变量/堆),坏处是需要同步(多个线程同时写同一变量会竞态)——这是锁、信号量、原子操作存在的根源。
- 私有栈的意义:每个线程的函数调用栈独立,A 线程调到
func3,B 线程调到func7,互不干扰。栈大小默认通常 1-8MB(Linux 默认 8MB),过多线程会耗尽虚拟地址空间(32 位下尤其明显)。
三、用户级线程 vs 内核级线程
线程是否对内核可见,决定了它能否多核并行、能否阻塞隔离:
- 用户级线程(ULT,User-Level Thread):由用户态线程库(如协程库、早期 Java 绿色线程)在进程内部管理,内核完全感知不到这些线程——内核只看到一个进程在跑。
- 优点:切换极快(在用户态改寄存器即可,不陷内核)、可自定义调度策略、数量不受内核线程上限限制。
- 缺点:①一个线程执行阻塞型系统调用(如
read)会阻塞整个进程(内核不知道还有别的线程);②不能利用多核(内核只给这个进程分一个 CPU 核,进程内的用户线程只能在该核上分时,无法并行)。
- 内核级线程(KLT,Kernel-Level Thread):内核直接管理,每个线程是一个独立的调度实体。Linux 中线程与进程统一用
task_struct表示(线程就是共享地址空间的"轻进程")。- 优点:①能多核并行(内核把不同线程调度到不同核);②一个线程阻塞不影响其它线程(内核可调度同进程的其它线程)。
- 缺点:创建、销毁、切换都要陷入内核,开销比用户级线程大;内核线程数量有上限。
- 现实主流是内核级:Linux pthread、Windows 线程、Java(现代 JVM,1:1 映射 OS 线程)都是内核级。协程(Go goroutine、Kotlin 协程、Python asyncio)则是用户级 + 多对多调度,兼顾轻量与并行。
四、多线程模型:用户线程如何映射到内核线程
用户线程与内核线程的映射关系,叫多线程模型,有三种:
| 模型 | 映射 | 并行 | 阻塞隔离 | 切换代价 | 代表 |
|---|---|---|---|---|---|
| 多对一 | 多个用户线程 → 1 个内核线程 | ❌ 不能 | ❌ 一个阻塞全阻塞 | 极快(用户态) | 早期 Solaris Green Threads |
| 一对一 | 每个用户线程 → 1 个内核线程 | ✅ 能 | ✅ 隔离 | 较大(陷内核) | Linux pthread、Windows、现代 Java |
| 多对多 | M 个用户线程 → N 个内核线程(N≤M) | ✅ 能 | ✅ 隔离 | 灵活(用户态切换为主) | Go goroutine(GMP)、Solaris M:N |
- 多对一:纯粹的用户级线程模型。切换最快,但任何阻塞都会卡死整个进程,且无法多核并行——现在很少单独用。
- 一对一:最简单可靠,主流 OS 都用它。每个用户线程绑定一个内核线程,内核直接调度,天然支持多核与阻塞隔离。代价是线程创建/切换要陷内核,且内核线程资源(栈、task_struct)不便宜,数量受限(Linux 默认线程数受内存与
ulimit -u约束)。 - 多对多(M:N):Go 的 goroutine 是典型——成千上万的 goroutine(用户级)被 GMP 调度器映射到少量 OS 线程(通常等于 CPU 核数)上跑。goroutine 切换在用户态极快(不陷内核),又能通过多个 OS 线程利用多核;当某个 goroutine 阻塞时,调度器把对应的 OS 线程让给它,用别的 OS 线程跑其它 goroutine——兼得轻量与并行。代价是运行时要实现复杂的调度器。
五、进程 vs 线程:核心对比
进程与线程的区别是面试第一高频题,从四个维度对比:
| 维度 | 进程(Process) | 线程(Thread) |
|---|---|---|
| 资源 | 独立地址空间、独立打开文件表、独立堆 | 共享进程的地址空间与资源;独立栈、PC、寄存器 |
| 基本单位 | 资源分配与保护的单位 | CPU 调度与执行的单位 |
| 切换代价 | 贵:换页表(CR3)、flush TLB、cache 失效,约数 μs | 便宜:不换页表,只换栈与寄存器,约亚 μs |
| 创建代价 | 贵:复制地址空间(即使 COW 也有开销)、分配资源 | 便宜:只分配栈与 TCB,共享进程资源 |
| 通信方式 | IPC:管道、消息队列、共享内存、信号量、socket | 直接共享内存(全局变量/堆),但要同步(锁/原子操作) |
| 崩溃影响 | 进程崩溃不影响其它进程(隔离好) | 线程崩溃(非法访存)会拖垮整个进程的所有线程 |
| 并发/并行 | 多进程可多核并行,但开销大 | 多线程可多核并行(一对一模型),开销小 |
| 适用场景 | 强隔离(浏览器多进程、服务隔离、Chrome 标签页) | 高并发共享资源(Web 服务器、数据库连接池) |
- 一句话总结:进程是重隔离的"重装甲单位",线程是轻共享的"快速突击单位"。需要强隔离、崩溃不扩散用进程;需要高并发、共享资源、廉价切换用线程。
- 混合架构:现代浏览器(Chrome)用"多进程 + 多线程"——每个标签页一个进程(隔离),进程内多线程处理 UI/网络/渲染。服务端常见"多进程 + 每进程线程池"(如 Nginx worker 进程 + 每进程异步 IO)。
六、多线程的优势与风险
- 优势:①响应性——UI 程序后台线程干活,主线程不卡顿;②资源共享——同地址空间通信便宜;③经济——线程创建/切换比进程便宜;④多核利用——多核 CPU 上多个线程真正并行。
- 风险(并发三难题):①竞态(Race Condition)——多线程同时读写共享变量,结果不确定,需加锁/原子操作;②死锁(Deadlock)——线程互相等待对方持有的锁,永久阻塞;③内存可见性——一个线程的写未及时对其它线程可见(CPU cache + 编译器重排),需内存屏障/volatile/同步原语。
下一步
理解了进程与线程的区别、用户级/内核级线程与多线程模型后,下一步进入并发同步(锁、信号量、条件变量)章节——学习如何安全地在多线程间共享数据;同时可参考参考页的进程 vs 线程速查大表。