线程、竞态与锁 · 操作系统
为什么两个线程给同一个计数器加数,结果会错?
课程目录为什么需要它
线程共享内存,所以协作很快,也很容易出错。竞态条件可能一千次里只错一次,你一加 print 调试它就消失了。一步一步看清楚它是怎么发生的,是第一道防线;锁是解决办法。
直觉理解
count += 1 看起来是一步,其实是三步:
- 把 count 读进寄存器
- 加 1
- 写回去
如果线程 A 读到 5,接着线程 B 也读到 5,两个都写回 6。有一次加法就丢了。
锁(互斥锁)把这三步变成临界区:同一时刻只能有一个线程在里面,另一个在门口等。
这一课通过交错线程的步骤来模拟线程,你能看到每一种可能的顺序。
定义
竞态条件
A bug where the result depends on the timing of threads: which one runs which step first.结果取决于线程时机的 bug:哪个线程先执行哪一步。
锁(互斥锁)
Only one thread can hold it at a time. Code between acquire and release runs without interference from other threads that use the same lock.同一时刻只有一个线程能持有它。在获取和释放之间的代码,不会被使用同一把锁的其他线程干扰。
示例
丢失一次更新
动手试试每个线程的加一都是三步。这个调度顺序把它们交错得很糟糕。
count = 5
regs = {}
def step(thread, op):
global count
if op == "read":
regs[thread] = count
elif op == "add":
regs[thread] += 1
else:
count = regs[thread]
print(thread, op, "count =", count)
for thread, op in [("A", "read"), ("B", "read"), ("A", "add"), ("B", "add"), ("A", "write"), ("B", "write")]:
step(thread, op)- L14两个线程都在对方写回之前读到了 5。
- L11A 写回 6,然后 B 又写回 6。
- L4加了两次,count 却只从 5 变成了 6。
B write count = 6
常见错误
「我测试的时候是好的」
竞态取决于时机。测试通过 100 次也说明不了什么;要分析所有可能的顺序。
加锁顺序不一致
线程 1 先拿锁 A 再拿 B,线程 2 先拿 B 再拿 A:它们可能永远互相等待。这就是死锁(第 5 课)。
小测验
1. 两个线程各执行一次 `count += 1`,从 0 开始,不加锁。可能得到哪些结果?
查看答案
答案:A
步骤交错时会丢一次更新(1);否则是 2。两个线程都至少写一次,所以不可能是 0。
2. 这段代码会输出什么?
count = 0
for thread in ["A", "B"]:
r = count
r += 1
count = r
print(count)查看答案
答案:A
这里两个线程一个接一个地运行,没有交错,所以什么都没丢。
动手练习
3 道练习,分三层:先预测程序的输出,再按新需求改程序,最后从零写一个。代码在浏览器里运行,提交马上判题。
- 预测输出数一数丢了几次
准确写出这段程序的输出。
- 改需求加上锁
run(schedule)回放各线程的步骤,每个线程都对共享计数器做read, add, write。加上锁:只有当没有别的线程持有锁时,线程才能read;它在read时拿锁,在write时放锁。某一步不能执行时,跳过并记下来,每次write之后再重试等待中的步骤。返回最终的计数。 - 从零写可能出错吗?
写函数
lost_updates(schedule):在一个从 0 开始的计数器上,回放由"T read"、"T add"、"T write"组成的步骤(不加锁),返回丢失了多少次加一:(写入次数)−(最终计数)。
本课术语
| 术语 | 中文 | 英文定义 |
|---|---|---|
| Thread | 线程 | A sequence of execution inside a process; threads share the process's memory. |
| Race condition | 竞态条件 | A bug where the result depends on thread timing. |
| Mutex (lock) | 互斥锁 | Lets only one thread at a time into a critical section. |
| Critical section | 临界区 | Code that must not run in two threads at once. |