线程理解
一、 进程与线程的本质区别
- 进程是资源分配的最小单位 :
- 内核为每个进程分配独立的虚拟地址空间(页表)、文件描述符表、信号处理规则等。
- 关键点:进程之间是"老死不相往来"的,内存隔离是默认状态。
- 线程是CPU调度的最小单位 :
- 线程是寄生在进程里的执行流。它没有独立的地址空间,只拥有维持执行所必需的最少资源:栈(Stack)、寄存器上下文(Context)、线程ID(TID)。
- 关键点:同一进程内的线程共享该进程的"家底"(内存、文件句柄),这使得它们之间的通信成本几乎为零,但也带来了数据竞争的风险。
二、 共享资源的底层透视(重点)
1. 代码段与数据段(Text & Data Segment)
- 底层实现 :所有线程共用同一套页表映射。这意味着,如果线程A修改了全局变量
global_count,线程B在下一次读取时,看到的是修改后的值(前提是缓存一致性协议生效)。 - 隐患 :
- 只读安全 :代码段是只读的,所以多线程调用同一个函数(如
printf)是安全的,不需要加锁。 - 读写危险:全局变量、静态变量位于数据段或BSS段,是所有线程的"公共厕所",必须加锁(Mutex)或使用原子操作(Atomic)保护。
- 只读安全 :代码段是只读的,所以多线程调用同一个函数(如
2. 文件描述符表(File Descriptor Table)
- 底层实现 :这是内核维护的一张表,记录了进程打开了哪些文件/Socket。线程共享这张表,意味着它们共享文件的偏移量(Offset)。
- 隐患 :
- 并发读写冲突:如果线程A和线程B同时向同一个文件描述符写数据,且没有加锁,数据会交错混乱。
- 生命周期管理 :线程A关闭了某个Socket(
close(fd)),线程B如果此时还在尝试读写这个fd,会直接报错EBADF。这是多线程网络编程中常见的Bug。
3. 信号处理(Signal Handling)
这是一个极易混淆的点,必须区分处理方式 与屏蔽字:
- 共享部分(处理方式) :
SIG_IGN(忽略)、SIG_DFL(默认)或自定义的处理函数(Handler)。这些是注册在进程控制块(PCB)里的。如果你在线程A里把SIGINT设为忽略,整个进程的所有线程都会忽略它。 - 私有部分(屏蔽字 Mask):每个线程可以决定自己"暂时不想收哪些信号"。这通常用于关键代码段的保护。
- 发送目标 :发给进程的信号(如
kill -9 pid),内核会随机挑选一个未屏蔽该信号的线程来执行处理函数。
4. 其他环境信息
- 当前工作目录(CWD) :进程级共享。线程A调用
chdir("/tmp"),线程B紧接着打开相对路径文件./data.txt,实际上打开的是/tmp/data.txt。这在复杂的多线程程序中是一个巨大的隐形炸弹。 - 用户ID/组ID :决定了进程的权限。一旦改变(如
setuid),所有线程的权限都会瞬间改变。
三、 线程私有成员
为了保证多线程能独立运行而不互相干扰,每个线程必须有自己的一套"私有成员
- 栈空间(Stack) :
- 这是线程最核心的私有资源。函数调用的局部变量、返回地址都存在这里。
- 注意:栈溢出(Stack Overflow)通常只会搞挂当前线程,而不会直接搞挂整个进程(除非破坏了关键的共享数据结构)。
- 寄存器上下文(Register Context) :
- 包括程序计数器(PC,指向下一条指令)、栈指针(SP)、通用寄存器等。
- 原理:当操作系统进行线程切换时,本质上就是把当前线程的寄存器值保存到内存(TCB中),再把下一个线程的寄存器值加载到CPU。这就是"上下文切换"。
- errno :
- C语言标准库的全局变量
errno在多线程环境下必须是线程私有的(通过 TLS 线程局部存储实现)。否则,线程A刚发生错误设置了errno,还没等读取,就被线程B的成功操作覆盖了,导致错误诊断完全失效。
- C语言标准库的全局变量
- 信号屏蔽字(Signal Mask) :
- 允许线程根据自己的执行阶段,动态地屏蔽某些信号,防止被打断。
四、 四种进程/线程模型的
结合你提供的图片,我们来看这四种形态在实际工程中的取舍:
| 模型 | 核心特征 | 适用场景 | 致命弱点 |
|---|---|---|---|
| 单线程进程 | 简单、无锁、串行 | 简单的脚本、嵌入式控制逻辑、早期的 DOS 程序 | I/O 阻塞即死机;无法利用多核 CPU。 |
| 多进程(多个单线程) | 隔离性最强、稳定性高 | Web 服务器的主控进程(如 Nginx Master)、安全性要求极高的系统 | 通信成本高(IPC);创建/切换开销大(需换页表)。 |
| 多线程(单进程多线程) | 切换极快、通信零成本 | 高并发服务器(Tomcat, Go Runtime)、GUI 应用 | "一损俱损"(一个线程野指针可能搞挂整个进程);调试困难。 |
| 多线程进程组 | 兼顾隔离与并发 | 现代浏览器(Chrome 每个标签页一个进程,进程内多线程)、大型数据库 | 架构极其复杂;资源消耗大。 |
线程是操作系统为了在"资源共享"和"独立执行"之间寻找平衡而创造的产物。