线程:并发执行单元与线程模型
线程是 CPU 调度的基本单位:同一进程内的多个线程共享地址空间与资源,各自拥有独立的栈、寄存器上下文与调度状态。
一句话定义
线程是 CPU 调度的基本单位:同一进程内的多个线程共享地址空间与资源,各自拥有独立的栈、寄存器上下文与调度状态。
为什么重要
进程解决了隔离,线程解决了"同一程序内并发"的效率问题:共享内存使数据交换零拷贝,切换成本低于进程切换(kp-006)。但共享也是一切并发 bug 的根源——从本节起,竞态、锁、死锁(kp-011~kp-015)的故事都发生在多个线程同时读改同一内存的场景里。
前置知识
kp-005(进程与 PCB);kp-006(切换成本差异的原因)。
核心概念
- 线程私有:栈、寄存器组、程序计数器、errno 等线程局部数据。
- 线程共享:地址空间(代码、全局变量、堆)、打开文件表、信号处理、子进程。
- KLT(内核级线程):内核直接调度,可利用多核,切换需陷入内核。
- ULT(用户级线程)/ 协程:用户态库自行调度,切换极廉价,但内核看不到,阻塞系统调用会拖累全部线程。
- M:N 模型:M 个用户线程映射到 N 个内核线程,试图兼得,复杂度高(Go 的 goroutine 调度器是其现代代表)。
- TCB:线程控制块,内核或库中记录线程状态的机构。
原理与机制
单线程与多线程进程的内存布局对比:
单线程进程 多线程进程(4 线程)
+-----------+ +-----------+
| 栈 | | 栈4 栈3 |
| (向下长) | | 栈2 栈1 | 每线程一个独立栈
+-----------+ +-----------+
| 空闲 | | 空闲 |
+-----------+ +-----------+
| 堆 | | 堆 | 共享:分配器需并发安全
| 全局数据 | | 全局数据 | 共享:需要同步保护
| 代码 | | 代码 | 共享(只读)
+-----------+ +-----------+
pthread 的最小骨架(C):
#include <pthread.h>
void* worker(void* arg) { return NULL; }
int main(void) {
pthread_t tids[4];
for (int i = 0; i < 4; i++)
pthread_create(&tids[i], NULL, worker, NULL);
for (int i = 0; i < 4; i++)
pthread_join(tids[i], NULL); /* 等待结束,等价于线程版 wait */
return 0;
}
编译 gcc -pthread demo.c;线程默认栈 8MB(虚拟地址空间预留,物理内存按需分配,呼应 kp-019)。
直观类比
进程像各自开火做饭的分租公寓(独立厨房互不影响但串门麻烦),线程像一家人共用一个厨房(共享冰箱省事,但两个人同时抢一个锅就会出事故——竞态条件,见 kp-011)。
实例或案例
ps -Lf <pid> # 列出进程的所有线程(LWP 号即内核线程 ID)
ulimit -s # 查看线程默认栈上限
top -H -p <pid> # 按线程维度观察 CPU 占用(Java/JVM 排障常用)
典型案例:Web 服务器"每连接一线程"模型在线程数过万时切换与内存开销失控,引出 kp-028 的事件驱动与 io_uring;"线程池为什么设成核数到 2 倍核数"是 kp-010 调度与 Amdahl 定律(kp-032)的联合应用题。
常见误区
- 线程数越多吞吐越高:超过物理核数后收益递减,切换与锁争用反而拉低性能。
- 把局部变量当线程安全:局部变量在私有栈上天然隔离,但通过指针泄漏出去就不再安全。
- 认为 ULT 已被淘汰:协程在 I/O 密集服务中复兴(goroutine、async/await),其代价与 ULT 时代一致——阻塞调用与调度公平性。
与其他知识点的关系
kp-011 起的整个并发模块都以多线程为背景;kp-029 容器以 cgroup 限额的就是一组进程/线程;kp-032 用 Amdahl 定律量化"多线程提速上限"。
延伸阅读
《现代操作系统》第 2 章线程节;Ousterhout 1996 年"Why Threads Are a Bad Idea"作为反方视角。
自测题
- 线程间共享哪些资源、私有哪些资源?
答:共享地址空间(堆、全局数据、代码)、打开文件表与信号处理;私有栈、寄存器上下文与程序计数器。
- KLT 与 ULT 的核心权衡是什么?
答:KLT 由内核调度、能用多核且单线程阻塞不拖累他人,但切换要陷入内核;ULT 切换廉价且调度灵活,但无法利用多核、一个线程阻塞系统调用会让整组线程停摆。
- 为什么多线程的堆分配器必须特殊设计?
答:多个线程并发 malloc/free 操作同一堆元数据,若不加锁或分区(如 tcmalloc 的线程缓存)就会产生竞态条件。