1.1 我是一个线程
沿线程的创建、就绪、运行、等待与回收追踪共享资源和独立上下文,用可重放实验定位并发故障。
学习目标
- 能解释线程与进程的归属关系,并沿创建、就绪、运行、等待、结束追踪上下文变化
- 能用共享计数器和调度事件说明读—改—写为何不是天然的原子操作
- 能在正常、边界和故障场景中保存首个偏离、资源所有者、恢复动作与复位证据
1.1 我是一个线程
本页依据刘欣《码农翻身》(2018 年第 1 版)及出版社公开书目信息,独立重构 1.1 我是一个线程。正文、代码、图示、实验和练习都是本课程重新设计的教学材料,不复制原书正文、插图、练习答案或代码。
线程不是一条脱离系统的故事线,而是进程资源合同内可调度的执行上下文。线程共享进程的地址空间和部分资源,却拥有自己的调用栈、寄存器状态、线程 ID 和调度状态;调度器可以在可运行线程之间切换,但切换不自动替共享数据建立同步协议。
三个会让线程模型失真的陷阱
五个目录节点到机制证据
1.1 我是一个线程
↡在进程资源上下文中拥有独立调用栈、寄存器状态、线程 ID 和调度状态的执行单位。不是把人物换成线程名,而是固定一个可观察的执行单位:它属于哪个进程、当前拿到什么 CPU 时间、是否访问共享状态,以及结束时谁负责回收。
线程的独立性只覆盖执行上下文,不自动覆盖数据所有权。读者应先为一个线程写出输入、所属进程和预期轨迹,再改变一个调度或同步条件,比较最早变化的节点。
初生牛犊
↡线程从创建请求进入可运行集合前后,建立线程 ID、初始栈、入口和所属进程的创建阶段。对应创建线程的机制证据。创建成功至少要记录线程 ID、入口参数、所属进程、栈和初始状态;只看到一个返回值,不能证明它已经执行。
如果创建后线程没有进入就绪队列,复核应停在创建到就绪的首个缺口,而不是把后续没有输出归咎于业务逻辑。
渐入佳境
↡线程进入可运行队列并获得调度机会,在时间片或主动让出后继续推进的运行阶段。对应就绪与运行的转换。调度器保存和恢复线程上下文,但不会替线程决定共享变量的业务不变量;获得 CPU 只证明执行机会存在。
记录就绪时间、运行时间片、上下文切换和写回顺序,才能把“没有运行”与“运行了但被覆盖”区分开。
虎口脱险
↡线程因锁、条件、I/O 或其他资源暂时不能推进,进入等待并在条件满足后重新竞争运行机会的阶段。对应等待资源的机制证据。等待不是失败,也不是自动完成同步;需要保存等待原因、资源所有者、唤醒条件和重新入队事件。
若线程持锁等待另一个持锁线程,首个不满足的资源依赖就是诊断入口。先解除或回滚依赖,再用同一输入重放,不能只增加超时掩盖死锁。
江湖再见
↡线程完成、取消或异常退出后,由所属进程确认未完成工作、等待者和最后引用并释放资源的结束阶段。对应结束与回收。线程返回不等于共享队列、锁、文件和上下文已经可释放;join、取消和异常路径都必须有明确的所有者。
把退出码、未完成任务、等待者、资源引用和回收时间放在同一条轨迹中,才能确认结束是可重放的终态。
线程执行合同
共享计数器的理想结果可以写成:
但这个式子只描述目标,不保证并发实现正确。每次增量都应绑定线程 ID、读取版本、调度事件和同步证据;若两个线程读取同一版本后分别写回,必须记录冲突,而不是用最终数字猜测历史。
state = baseline(thread_id, process_id, counter=0)
for event in scenario:
state = transition(state, event)
assert owner_and_invariant_hold(state)
assert reset(state) == baseline(thread_id, process_id, counter=0)这段草图的边界是验证轨迹,不是一个可直接替代线程库的实现。实际复核需要保存输入、线程上下文、状态转换、同步协议、首差、最终结果和复位结果。
五步复核线程生命周期
1. 创建线程并固定所有权
冻结进程 ID、线程入口、参数、栈、共享变量和初始状态。先预测线程何时进入就绪队列,以及创建失败时哪些资源必须回收。
Lab
线程交错与回收实验
只改变一个调度或同步条件,观察线程状态、共享版本和所有权判定。
T1 获得 CPU,原子增量后结束
T1: read v0 → add 1 → write v1 → join → reclaim
判定
通过:所有者明确,计数和引用闭合
当前场景:基线运行;记录线程 ID、进程归属、调度顺序、共享版本和回收事件。
正常、边界与故障证据
| 样本 | 只改变的变量 | 预期判定 | 必存证据 |
|---|---|---|---|
| 正常 | 一个线程完成增量并由所有者回收 | 状态按合同推进,资源闭合 | 线程 ID、上下文、版本、回收记录 |
| 边界 | 时间片、队列容量、锁等待或任务数接近阈值 | 在资源边界停住并给出唤醒或回退 | 阈值、等待者、所有者、替代路径 |
| 故障 | 一次读—改—写交错或一次回收动作失败 | 首个偏离可定位,修复后可重放 | 首差、调度顺序、同步证据、复位结果 |
故障诊断:先找最早的合同违约
- 确认归属:核对线程 ID、进程 ID、栈、共享资源和当前状态;跨进程或已结束线程的访问先暂停。
- 确认交错:按时间顺序排列读取、计算、写回、锁、等待、唤醒和切换;找第一处不满足同步协议的事件。
- 确认恢复:决定是补锁或原子操作、解除资源依赖、取消线程还是延迟回收;保留修复前后的同一输入。
- 确认终态:检查计数版本、队列、锁、文件、等待者和最后引用,确保复位后没有把上轮污染带入下一次实验。
术语表
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 1.1 我是一个线程
在进程资源上下文中拥有独立调用栈、寄存器状态、线程 ID 和调度状态的执行单位。
- 初生牛犊
线程从创建请求进入可运行集合前后建立身份、入口和初始状态的阶段。
- 渐入佳境
线程进入可运行队列并获得调度机会,在时间片或主动让出后继续推进的阶段。
- 虎口脱险
线程因锁、条件、I/O 或其他资源暂时等待,并在条件满足后重新竞争运行机会的阶段。
- 江湖再见
线程完成、取消或异常退出后,由所有者确认引用并释放资源的结束阶段。
练习
练习
问题 1: 两个线程都把计数器从 0 加到 1,最终结果为 1,最应该先检查什么?
问题 2: 一个线程已经返回,但另一个线程仍持有它使用的共享队列,能否立即释放队列?
问题 3: 线程一直处于等待状态,你如何区分资源耗尽和死锁?
本页小结
线程的关键不是把执行线拟人化,而是能说明它属于哪个进程、何时获得 CPU、为何等待、怎样保护共享状态,以及结束后谁负责回收。完成标准是用同一份输入重放五个生命周期节点,在故障轨迹中定位首个合同违约,修复后证明计数、所有权和复位结果都闭合。