
计算机基础的系列笔记写到第11篇这次我打算把“进程与线程”这块彻底讲透。前几篇大多在聊硬件、二进制、数据结构这些相对静态的东西从这篇开始要进入操作系统最核心的动态部分程序到底是怎么跑起来的。对于刚接触计算机基础的新手来说进程和线程往往是第一个让人感到有点抽象、又无处不在的概念同时也是后续理解并发编程、系统性能优化的地基。这篇文章就结合我的学习笔记和实际调试经历把这两个概念掰开揉碎讲清楚它们的原理、区别以及你在实际碰代码时大概率会遇到的那些坑。1. 内容整体设计与思路拆解1.1 为什么系列笔记的第11篇要选进程与线程写笔记这件事最忌讳的就是东一榔头西一棒槌。我的这个“计算机基础笔记”系列前10篇已经覆盖了计算机组成、数据表示、简单算法与数据结构。第11篇选择进程与线程是因为它恰好处于“硬件基础”和“软件实践”的衔接点CPU怎么调度计算任务、内存如何分配与隔离、代码如何并发执行这些问题的答案全都要从进程和线程说起。很多人觉得这概念太理论离日常开发很远。但实际情况是哪怕你只是用程序打开一个文件、发起一次网络请求背后都躲不开进程和线程的协作。理解它们不是为了考试而是为了以后排查程序卡死、内存飙升、CPU占用异常时脑子里能有一个清晰的模型知道该往哪个方向去找问题。1.2 思路拆解从“是什么”到“怎么用”再到“踩坑”我这篇笔记的写法不是把教科书上的定义抄一遍而是按照“概念模型 - 实操观察 - 故障排查”这条线展开。先建立一个足够直观的心理模型进程是资源的容器线程是执行的单位。然后用命令行和简单的代码让读者亲眼看到进程和线程的存在。最后再用几个真实到让人头秃的并发bug把同步、死锁、竞态这些概念从纸上拉回到现实里。这样的设计有一个明显的好处每个知识点都能落到具体的观察动作上。比如你敲一条ps命令看到一堆PID再对比不同线程的PID概念自然就立体了。比单纯背诵“进程是资源分配的最小单位线程是CPU调度的最小单位”要有用得多。1.3 适合谁读读完能收获什么这篇内容适合三类人正在自学计算机基础的学生、刚接触编程的转行者、以及写了些业务代码但对系统行为一知半解的开发者。读完以后你应该能够清楚回答一个进程在内存里长什么样、线程之间为什么需要锁、死锁是怎么产生的、为什么有时候多线程反而更慢。我尽量不堆术语所有复杂的地方都会用生活里的小例子类比确保第一次接触的人也能顺畅读完。2. 核心概念解析进程、线程和它们背后的操作系统逻辑2.1 进程就是运行中的程序以及它独占的那一亩三分地首先明确一个最基础的点程序是静态的代码文件进程是程序跑起来之后的动态实体。你写了一个文档编辑器硬盘上的编辑器文件是程序你双击运行它操作系统会在内存里为它分配一块独立的地址空间加载代码和数据然后开始执行指令这时候就诞生了一个进程。进程拥有几个关键资源独立的虚拟地址空间、打开的文件描述符表、环境变量、信号处理器等。其中地址空间隔离是进程最重要的特性——两个进程哪怕跑在同一台机器上彼此也看不到对方的内部数据。比如一个绘画软件崩了正常情况下不会拖垮旁边的音乐播放器原因就是进程级别的水密隔舱。我常跟初学者打一个比方进程就像一家独立的餐厅有自己的后厨、仓库和财务室装修得再差另一个餐厅也不能直接进你的后厨搬调料。这种隔离给了我们安全性和稳定性代价是进程之间通信非常麻烦通常要靠管道、消息队列、共享内存这些操作系统提供的“外卖窗口”来完成。2.2 线程进程内部的小分队共享资源但独立干活如果说进程是餐厅那线程就是餐厅里的厨师和服务员。同一个进程的多个线程共享这块内存区域和文件资源但每个线程有自己的执行上下文一组寄存器、栈、程序计数器。也就是说线程们看得见彼此干活能直接操作同一批变量这既是便利也是矛盾的源头。线程的创建代价比进程小得多因为不需要重新分配整套地址空间只是多增加一个执行流。但代价是线程之间没有天然隔离一个线程越界写坏了堆区整个进程都会崩溃。这也是为什么多线程程序调试起来特别容易“殃及池鱼”。这里有一个隐藏知识点操作系统真正调度执行的最小单位是线程而不是进程。CPU分配到的是线程的时间片进程只是给这些线程提供了公共资源池。很多教材说法容易误导人但把“调度看线程资源看进程”这句话记住后面很多问题都能想通。2.3 状态切换从“就绪”到“运行”再到“阻塞”的过程笔记里绕不开的还有进程/线程的状态机。一张典型的进程状态图包含新建、就绪、运行、阻塞、终止五大状态。看起来简单但每个状态迁移背后都有对应的系统调用和调度器行为。例如一个线程等待磁盘I/O时进入阻塞状态主动让出CPU等I/O完成又被唤醒到就绪队列等待下一次调度。我用排队打饭来类比就绪队列是等待叫号的人CPU是打饭窗口阻塞则是那人中途去了洗手间叫多少次号都不应答。理解状态迁移的最大价值在于你能解释一个奇怪现象为什么“卡死”的程序CPU占用率却是0%因为它在阻塞等I/O根本没消耗CPU时间片。2.4 并发与并行别看长得像实际差很多很多人把并发和并行混着用但必须要分清。并发是同一时间段内多个任务交替推进更像是只有一个打饭窗口时大家轮流打并行是同一时刻多个任务同时执行需要多核CPU。一个单核CPU永远只能并行0个任务但它可以并发跑几千个线程靠的就是快速切换。从实际感受来说并发能提升资源利用率比如等待I/O时让出CPU给别的任务但在极端情况下如果切换过于频繁线程之间为了抢锁互相等待反而会导致性能下降。所以“多线程一定更快”是新手常见的误区我在后面实操部分会专门用一个例子来展示。3. 实操过程与核心环节实现亲眼看到进程和线程3.1 用命令行观察进程ps、top与系统监控的真实输出概念讲完最好马上去机器上动手验证。在Linux环境下我最常用的是ps -ef查看静态快照用top或htop观察动态刷新数据。以ps -ef为例你会看到PID、PPID、CPU、MEM、启动时间、执行命令这些字段。PID是进程的唯一标识PPID是它的父进程ID这里牵出另一个知识所有进程的祖先最终都能追溯到系统的1号进程。top观察的又是另一层信息。它能看到每个进程的实时CPU占用率、内存占用以及耗时。比较有意思的一点是你在top里看到的进程状态列经常是Ssleeping或Rrunning对应的是刚才讲的状态模型。实际去跑一次比对着屏幕去理解状态迁移印象会深得多。在Windows上则可以用任务管理器或PowerShell里的Get-Process命令。不过Linux的top更直观也更容易展开讨论。我笔记里贴了一段典型的top输出标注出每一列的含义并建议初学者每天早中晚各跑一次观察不同时间点的进程组成猜一猜哪些是系统服务、哪些是用户应用。3.2 用系统工具查看线程从单进程看到多条执行流很多人不知道系统命令除了看进程也能看线程。Linux下用ps -eT或者top -H就能把每个进程里的多个线程列出来。你会看到同一PID下面有好几个TID线程ID各自占不同的CPU。举个例子一个Java应用往往有成百上千个线程有些在跑GC有些在处理网络请求有些纯粹在睡觉等任务。看到这些之后“线程是执行单元”这句话就再也不是空头概念了。在Python里你可以用os.getpid()和threading.get_ident()打印进程ID和线程ID直观地观察它们的关系。我习惯让一个程序里开三个线程各自循环打印自己的ID你会发现进程PID只有一个但线程ID各不相同。这种小实验成本极低但对抗概念模糊特别有效。3.3 代码实操从单线程到多线程的最小示例为了把概念落地我写了一个极简的Python示例。threading.Thread创建两个线程每个线程执行一个循环然后打印结果。第一版是单线程按顺序执行第二版是两个线程交替执行观察输出顺序的变化。从实验结果看两线程版输出会交错出现有时这个先有时那个先完全看操作系统调度。不过要提醒一点Python的threading因为全局解释器锁多线程在CPU密集场景下不会加速这是解释器层面的限制但在I/O密集场景比如读文件、网络请求里仍然有效。所以我写这个示例的初衷是观察线程的“存在感和执行流切换”而不是示范加速。真正想用多核加速计算Python里应该考虑多进程模块。3.4 参数与性能观察为什么有时候线程数越多越慢实操里最容易遇到“开了一堆线程反而更慢”的情况。我在笔记里记录了这样一个实验让四个线程同时对一个共享计数变量做自增100万次不用任何锁保护。结果数字跑完不是预期的400万而是一个明显偏小的数。原因就在于自增操作并非原子操作在底层其实要分成“读原值、加一、写回去”三步。多个线程可能同时读到同一个旧值各自加一再写回白白覆盖了彼此的进展。这个实验价值极高它会让你第一次真切体会到“竞态条件”的杀伤力。于是下一步自然引出了锁。用threading.Lock保护临界区之后计数结果终于正确了但执行时间比单线程还要慢。因为锁的获取和释放本身就是开销加上线程切换这笔账逐渐变成负数。这说明了一个核心理念多线程不是免费的加速卡而是需要精确设计的技术。4. 常见问题与排查技巧实录并发问题排查与避坑指南4.1 死锁两个人互相等对方先让路程序就彻底停住死锁是线程同步中最经典的问题。每个线程都拿着一把锁又等着对方释放另一把锁导致所有人都在无限等待。我实际调试过一个模拟项目XA线程持有锁1准备申请锁2B线程持有锁2准备申请锁1结果两边就僵住了。程序既不崩溃也不退出CPU占用很低但所有请求都无响应这是死锁的典型画像。排查死锁我常用两种手段。一是用jstack或pstack这类工具抓线程堆栈看每个线程的锁等待关系能直接在输出里看到“waiting to lock”的字样。二是从设计上避免比如保证所有线程按同一顺序加锁或者用“尝试获取锁拿不到就放弃手里已有的锁”的超时机制。死锁在代码写出来后不一定马上出现往往是压测或线上高并发时才冒头所以初学阶段就建立锁顺序意识特别重要。4.2 竞态条件结果取决于运气最让人头皮发麻前面提到的计数实验就是竞态条件的一个缩影。它不像死锁那样容易复现有时候跑十次错一次有时候次次错让人摸不着头脑。我见过某新手同学写的生产者-消费者程序生产者的数据和消费者的数据偶尔对不上查了几天才发现是没有对缓冲区加锁。这种“大概率正常但偶尔抽风”的bug是所有系统工程师最讨厌的类型。排查竞态第一要务是复现。可以故意增加线程数量、加大循环次数或者插桩打印变量中间值。第二步是检查所有共享可变数据的访问是否都在同一把锁的保护之下。还要注意即使单条指令是原子的多条指令的组合也可能被别的线程打断。真有需要可以查一下原子操作、内存屏障这些底层机制但初学者先把锁用好就足够了。4.3 线程安全设计加锁范围大了慢小了错怎么平衡加锁不是万灵药锁的粒度直接决定程序性能。锁太大比如把整个函数都包进with lock里等于让线程们串行执行多线程优势荡然无存锁太小又可能保护不到完整的数据变更新序列。我自己的原则是先利用“不可变对象优先局部变量优先”来减少共享数据然后只对最小的临界区加锁。举个实际经验维护一个用户列表线程A要“读取列表、添加一项、返回新数量”。如果只给添加操作加锁那读取时可能看到中间状态。这时候就要给整个读取修改操作一把锁而不是只锁写。这个“锁临界区”的判断需要在写代码时反复推敲也是从初学者进阶的必经关卡。4.4 内存与资源泄漏线程开了一堆关不掉的坑与锁没有直接关系但同样常见的坑是线程泄漏。曾有个同学用多线程处理一批任务每处理一个任务就新建一个线程任务完成后线程对象却一直保留在容器里最终内存和句柄数一路飙升系统直接无响应。正确做法是用线程池来控制并发上限任务结束后把资源交回池子里复用而不是无限制地造新线程。我去查热点新闻时也看到个有意思的比喻线程池就像员工外包团队一次性签约固定人数有活就分配没活就待命比每来一个活就临时招人靠谱得多。所以无论是哪种语言尽量优先使用成熟的线程池组件而不要自己在业务代码里裸造线程。4.5 避坑速查表几个常见症状与对应思路我把实际工作中遇到的高频问题整理成一张速查表方便你按图索骥。表格里的症状都来自真实调试现场思路也是亲测有效。症状可能原因排查思路程序CPU占用为0%但卡死线程处于阻塞状态可能在等锁或等I/O抓线程堆栈查看锁等待关系CPU占用高但程序响应慢线程在激烈竞争锁或存在忙等改写状态检查临界区是否过大统计锁等待时间结果偶尔对、偶尔错竞态条件共享变量未加锁增加循环次数复现检查所有共享变量访问内存持续增长线程或资源对象未释放检查线程生命周期改用线程池多线程比单线程慢锁竞争过度或操作本身太轻量评估能否减少同步或改用多进程5. 实操心得如何把这个笔记变成你自己的能力5.1 动手实验的优先级比“读懂原理”更高如果让我给一条最核心的学习建议那就是原理看不懂也别硬扣先照着案例把代码敲出来、把命令跑通再回头看书很多概念会自己通。我第一次接触进程状态机时满脑子浆糊但当我写了个程序去模拟阻塞等待再用top看到它状态变成S之后那一瞬间就觉得“哦原来这就是阻塞”。建议你像我一样准备一个专门的实验目录把进程观察、多线程计数、死锁复现这些小例子都聚在一起。每次学一个新概念先跑最小案例再做一点小的改变观察变化。不要急着超越先把基础的本能记牢。5.2 用笔记沉淀“坑位”而不是抄概念我的系列笔记里到处是“当时卡了多久”的草稿比工整的概念抄录有用得多。每次排查完一个诡异问题我会记录三件事现象是什么、我当时怎么猜的、最终根因是什么。这能把自己原本模糊的假设和现实假设做对照慢慢就会形成对系统的直觉。进程和线程这块尤其如此因为并发问题几乎不会以教科书的样子出现只靠读不靠记是不够的。5.3 下一步扩展从进程线程到网络与分布式笔记写到第11篇进程与线程这层基础已经建立出轮廓了。之后自然的方向是看内存管理、I/O模型再到网络编程。你会发现很多网络框架经常号称“高并发”但底层的本质还是在管理线程和I/O事件的调度关系。把这一篇的内容搞扎实后续学任何应用层的并发方案都会顺畅得多。最后再分享一个小技巧遇到进程或线程相关的怪问题先别急着改代码。先用操作系统自带的工具把现状看清楚再动手。大多数bug都不是藏在你的代码逻辑里而是藏在你看不见的调度与资源竞争里。眼睛看见了问题就解决了一半。