LabHub
学习 学习路径 课程

操作系统

线程 — 分得便宜,但多了要守的东西

在 LabHub 中继续学习

一句话总结

线程是一条执行流:它与同一进程内其他线程共享代码、数据、堆和打开的文件,只独立拥有栈与寄存器。共享既是性能来源,也是缺陷来源。

概念图: 一句话总结 · 为什么需要它 · 它如何工作 · 在实际项目中

为什么需要它

进程提供隔离,代价却是通信昂贵。不同进程拥有不同地址空间,传递数据需要管道、共享内存等额外机制。对于 Web 服务器这类多个请求要共同查看同一份数据的程序,这项成本会成为负担。

线程放弃部分隔离,换取消除这项成本。由于共享同一地址空间,只需传递一个指针就能共享数据。

它如何工作

同一进程内的线程共享什么、各自拥有什么,可以整理成下面这张表。

共享的内容 独立拥有的内容
代码区、全局数据、堆
打开的文件描述符 寄存器、程序计数器
信号处理程序、当前目录 线程局部存储

线程上下文切换比进程更便宜,原因也在这里。地址空间保持不变,因此无需切换页表,也不必清空整个 TLB。

线程实现模型曾分为三类。只在用户态管理的多对一模型切换成本极低,但一个线程执行阻塞系统调用时,整个进程都会停住。Linux 与 Windows 采用的一对一模型为每个用户线程对应一个内核线程,能够真正并行运行,也能独立阻塞,代价是线程创建相对昂贵。多对多模型试图折中两者,却因实现复杂而没有成为主流。今天的 goroutine 与 coroutine,可以看作在语言运行时层重新解决这场争论的尝试。

在实际项目中

使用线程的那一刻,就会产生一项新责任:同步共享数据。两个线程同时递增同一个变量,落实到机器指令是读取、加一、写回三个步骤;如果中间发生切换,就会丢失一次更新。这类问题在测试中很难捕捉,往往只在负载升高时出现。

还有一点:“增加线程就会更快”的直觉,一旦超过 CPU 核心数就会失效。可运行线程多于核心时,会彼此抢占,只增加上下文切换与缓存污染。CPU 密集型任务的线程数可以从接近核心数开始;I/O 密集型任务则可以根据等待时间适当增加。不作区分,直接复制“线程池大小 200”之类的设置,是常见事故来源。

接下来的测验要确认什么

确认你能否区分线程共享的内容与独立拥有的内容,并判断这些选择会引发哪些缺陷。