人人都能学编程
English 开始学习
核心 · 操作系统第 2 课 / 共 6 课免费阅读

线程、竞态与锁 · 操作系统

为什么两个线程给同一个计数器加数,结果会错?

课程目录

为什么需要它

线程共享内存,所以协作很快,也很容易出错。竞态条件可能一千次里只错一次,你一加 print 调试它就消失了。一步一步看清楚它是怎么发生的,是第一道防线;锁是解决办法。

直觉理解

count += 1 看起来是一步,其实是三步:

  1. 把 count 读进寄存器
  2. 加 1
  3. 写回去

如果线程 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)
逐步执行
  1. L14两个线程都在对方写回之前读到了 5。
  2. L11A 写回 6,然后 B 又写回 6。
  3. L4加了两次,count 却只从 5 变成了 6。B write count = 6

常见错误

「我测试的时候是好的」

竞态取决于时机。测试通过 100 次也说明不了什么;要分析所有可能的顺序。

加锁顺序不一致

线程 1 先拿锁 A 再拿 B,线程 2 先拿 B 再拿 A:它们可能永远互相等待。这就是死锁(第 5 课)。

小测验

1. 两个线程各执行一次 `count += 1`,从 0 开始,不加锁。可能得到哪些结果?

  1. A1 或 2
  2. B总是 2
  3. C0、1 或 2
查看答案

答案:A

步骤交错时会丢一次更新(1);否则是 2。两个线程都至少写一次,所以不可能是 0。

2. 这段代码会输出什么?

count = 0
for thread in ["A", "B"]:
    r = count
    r += 1
    count = r
print(count)
  1. A
    2
  2. B
    1
  3. C
    0
查看答案

答案:A

这里两个线程一个接一个地运行,没有交错,所以什么都没丢。

动手练习

3 道练习,分三层:先预测程序的输出,再按新需求改程序,最后从零写一个。代码在浏览器里运行,提交马上判题。

  1. 预测输出数一数丢了几次

    准确写出这段程序的输出。

  2. 改需求加上锁

    run(schedule) 回放各线程的步骤,每个线程都对共享计数器做 read, add, write。加上锁:只有当没有别的线程持有锁时,线程才能 read;它在 read 时拿锁,在 write 时放锁。某一步不能执行时,跳过并记下来,每次 write 之后再重试等待中的步骤。返回最终的计数。

  3. 从零写可能出错吗?

    写函数 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.