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数据分析助手
为业务和科研数据的快速处理提供Python数据清理、统计分析和可视化建议。
下载- 推荐用
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 # 这条线现在安全了为什么有时候加?
Lock 还出问题?
锁本身无法掩盖设计缺陷。最常被忽视的是,你认为资源被锁定了,但实际上并没有锁定真正并发修改的部分。
立即学习“Python免费学习笔记(深入);
- 锁定局部变量无效:
def f(): x = []; with lock: x.append(1)——x每个线程都是自己的,根本不需要锁定 - 锁定错误对象:例如使用
threading.Lock()锁住一个queue.Queue例子-没有必要,因为Queue线程本身是安全的 - 条件竞争不覆盖整个路径:例如,只锁定写作,但阅读操作也取决于值的一致性(如“检查后执行”模式),因此必须将检查+执行包裹在一起
with lock: - 误以为锁可以解决 IO 或网络延迟引起的逻辑冲突(例如,两个线程都发现有足够的余额,然后扣除)-这是业务层需要权力或交易,而不是
Lock的职责
真正困难的不是写作 with lock:,相反,准确识别哪个代码属于临界区域,哪些变量是共享和可变的,以及锁定的生命周期是否严格符合资源访问周期。在多线程调试中,竞争态度往往是偶然的,难以复制,因此在设计阶段应该明确共享边界,而不是等待 bug 再补锁。