threading.lock 能解决竞态条件,但必须锁住共享可变对象的完整访问路径;counter += 1 等非原子操作需加锁,推荐用 with lock: 确保自动释放,且锁实例必须全局唯一并覆盖全部临界区。

threading.Lock 能解决竞态条件,但必须用对地方——锁住的是共享资源的访问路径,不是线程本身。
什么时候必须加 Lock?
当多个线程同时读写同一个可变对象(比如 list、dict、全局计数器 counter)且操作非原子时,就会出问题。典型现象是:结果值比预期小、列表长度异常、字典键丢失。
- 常见错误场景:
counter += 1看似一行,实际分三步:读取counter→ 计算 +1 → 写回;两线程交错执行就丢一次更新 - 不是所有共享都需锁:
str、int等不可变对象被重新赋值不涉及竞态,但它们的容器(如list.append())会修改内部状态,必须锁 - CPython 的 GIL 不保证逻辑正确性——它只保释单个字节码原子,而
counter += 1对应多条字节码
Lock 怎么用才不出错?
核心原则:锁的粒度要覆盖「从读到写」的整个临界区,且必须成对出现。漏掉 release() 或提前 release() 都会导致死锁或数据混乱。
快速生成专业的 Python 脚本和应用代码。一键创建完整项目结构,支持CLI、API、爬虫、Bot、Django等多种项目类型,包含完整的项目结构、配置文件、依赖管理、测试、README和文档。
- 推荐用
with lock:语法——自动处理获取和释放,即使抛异常也不忘解锁 - 不要手动调用
lock.acquire()后忘记lock.release(),尤其在有return或异常分支的函数里 - 锁对象必须是同一个实例:不同线程用各自的
Lock()毫无意义;全局共享或作为类属性传入 - 避免嵌套锁或交叉加锁顺序,否则极易死锁(例如线程 A 锁
lock_a再等lock_b,线程 B 反过来)
import threading <p>counter = 0 lock = threading.Lock() # 全局唯一锁实例</p><p>def worker(): global counter for _ in range(100000): with lock: # 自动 acquire/release counter += 1 # 这行现在安全了 </p>
为什么有时候加了 Lock 还出问题?
锁本身不能掩盖设计缺陷。最常被忽略的是:你以为锁住了资源,其实没锁住真正被并发修改的部分。
- 对局部变量加锁无效:
def f(): x = []; with lock: x.append(1)——x是每个线程自己的,根本不需要锁 - 锁住错误对象:比如用
threading.Lock()锁住一个queue.Queue实例——没必要,因为Queue本身已线程安全 - 条件竞争未覆盖全路径:例如只锁了写,但读操作也依赖该值一致性(如“检查后执行”模式),就得把检查+执行一起包进
with lock: - 误以为锁能解决 IO 或网络延迟导致的逻辑冲突(比如两个线程都查到余额充足然后扣款)——这是业务层需要幂等或事务,不是
Lock的职责
真正难的不是写上 with lock:,而是准确识别哪段代码属于临界区、哪些变量是共享且可变的、以及锁的生命周期是否与资源访问周期严格对齐。多线程调试时,竞态往往偶发且难以复现,所以设计阶段就要明确共享边界,而不是等出 bug 再补锁。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










