(四)共享数据、竞争与锁
1. 两次更新覆盖了同一份旧值
两个线程都想把计数加一。把“读取旧值”和“写回新值”拆开,再用 Barrier 让双方都读完才写,能稳定看见一次更新丢失:
# 项目文件:demos/thread_race.py
# 错误示例:Barrier 等到两边都读取旧值,再同时进入写回阶段。
from threading import Barrier, Thread
counter = 0
both_read = Barrier(2)
def increase():
global counter
old = counter
both_read.wait()
counter = old + 1
if __name__ == "__main__":
threads = [Thread(target=increase) for _ in range(2)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
print(counter)双方读到的 old 都是 0,各自写回 1,最终输出 1。它们确实各执行了一次写入,但没有保留两次更新。第五章两份旧文件快照先后保存时也有类似的问题。
2. 把读、改、写放进同一次互斥范围
# 项目文件:demos/thread_lock.py
# Lock 保护完整的读取和写回;第二个线程取得锁后读到的是更新后的值。
from threading import Lock, Thread
counter = 0
lock = Lock()
def increase():
global counter
with lock:
old = counter
counter = old + 1
if __name__ == "__main__":
threads = [Thread(target=increase) for _ in range(2)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
print(counter)这次输出 2。锁保护的是一组需要作为整体发生的动作。若只把最后一行写入加锁,两个线程仍可能先读到同一个旧值。上个例子的 Barrier 也不能放进这份锁内等待第二个线程:第一个持锁等待,第二个拿不到锁,两边会互相等住。
可以继续减少共享:工作线程只返回自己处理的书目,主线程统一汇总;把要传递的结果放进队列,而不是让所有线程随时修改同一个业务对象。