threading.lock是解决python多线程数据竞争最常用、最可靠的手段,但必须确保锁对象全局唯一、临界区完整包裹、始终用with语句自动管理;counter += 1丢数据是因为其被解释为“读-改-写”三步非原子操作,多线程交错执行导致覆盖。

直接说结论:threading.Lock 是解决 Python 多线程数据竞争最常用、最可靠的手段,但必须用对——锁对象要共享、临界区要包全、with 语句不能少。
为什么 counter += 1 会丢数据?
这不是 Python 的 bug,而是所有语言都面临的“读-改-写”非原子性问题。CPython 中 counter += 1 实际拆成三步:读取当前值 → 在寄存器里加 1 → 写回内存。两个线程如果交错执行这三步,就会出现“都读到 100,都算出 101,都写回 101”,最终结果还是 101,而不是预期的 102。
常见错误现象包括:
- 多线程累加后总数远小于理论值(比如 5 个线程各加 10 万,结果只有 32 万)
- 字典
dict[key] = value报KeyError或RuntimeError: dictionary changed size during iteration - 列表
append()后长度忽大忽小,或内容重复/缺失
threading.Lock 必须是同一个实例
锁没效果,90% 是因为用了多个锁对象。比如在每个线程里 lock = threading.Lock(),或者在类的 __init__ 里创建 self.lock —— 这些锁彼此无关,完全起不到互斥作用。
正确做法只有一条:锁对象必须定义在模块级、类级,或作为全局变量,确保所有线程访问的是同一把锁。
- ✅ 模块级锁:
lock = threading.Lock()放在文件顶部 - ✅ 类级锁:
class Counter: lock = threading.Lock() - ❌ 实例级锁:
def __init__(self): self.lock = threading.Lock() - ❌ 局部锁:
def increment(): lock = threading.Lock(); with lock: ...
临界区要包含整个依赖共享状态的操作
只锁 counter += 1 这一行是不够的。只要逻辑上“先读再判再改”,就必须整段包裹。否则会出现 TOCTOU(time-of-check to time-of-use)问题。
例如下面这段代码即使加了锁也危险:
if counter <p>正确写法是:</p><pre class="brush:python;toolbar:false;">with lock:
if counter
- 对字典做
if key not in d: d[key] = val,必须整个判断+赋值一起锁 - 对列表做
if item in my_list: my_list.remove(item),同样要锁住整块 - 避免用
lock.acquire()/lock.release()手动配对——异常时容易漏掉release()导致死锁
别用锁保护 queue.Queue 或 threading.local()
queue.Queue 本身已内置线程安全实现,put() 和 get() 都是原子操作,额外加锁反而可能引发阻塞或逻辑混乱。
threading.local() 提供的是线程隔离存储,根本不存在共享,自然也不需要锁——它适合存日志 ID、数据库连接等“每线程一份”的数据。
真正需要锁的,是那些明确被多个线程共同读写的对象:
- 模块级全局变量(如
config = {}) - 类变量(如
MyClass.total_requests = 0) - 传给多个线程的同一字典/列表实例(如
shared_data = {"status": "idle"})
最容易被忽略的一点:锁不能解决 GIL 带来的 CPU 密集型任务并行瓶颈,它只保证共享数据不被破坏。如果你发现加了锁性能仍差,问题可能不在同步,而在任务本身是否适合多线程。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











