类变量在多线程下并发修改必然导致数据竞争,因其存储在类对象__dict__中、被所有线程共享,且“读-改-写”非原子;必须用类级锁(如lock作为类变量)保护整个临界区,不可用实例锁或局部锁。

类变量在多线程下被并发修改,必然导致数据竞争——它和全局变量一样,属于所有线程共享的同一块内存,不加锁就写,结果一定错。
为什么 class variable 也会出竞态?
类变量(如 MyClass.counter)存储在类对象的 __dict__ 中,所有实例和线程共享同一份引用。哪怕你用 self.__class__.counter += 1,背后仍是“读-改-写”三步非原子操作。现象和全局变量完全一致:最终值远小于预期、偶发 AttributeError(因其他线程删了属性)、甚至 SystemError(底层结构被破坏)。
- 常见错误写法:
self.__class__.counter += 1或MyClass.counter += 1直接裸写 - 看似“属于类”,实则没任何线程安全机制——Python 不会自动给类变量加锁
- 即使所有线程只调用同一个类的同一个方法,只要修改的是类变量,就需同步
Lock 必须绑定到类本身,不能放实例里
锁对象必须是类级(class-level)的,且所有线程共用同一把锁。如果在 __init__ 里创建 self.lock = threading.Lock(),每个实例一把锁,完全无效。
- ✅ 正确:在类定义外或类体内定义为类变量,例如
lock = threading.Lock() - ✅ 正确:用模块级锁,然后在类方法中引用
- ❌ 错误:在实例方法里每次 new 一个
threading.Lock() - ❌ 错误:用
@classmethod但锁是局部变量(作用域不对)
临界区要包住整个读-改-写逻辑
不只是 += 那一行要锁——任何依赖当前值做判断再修改的操作,都必须整段包裹,否则出现 TOCTOU(Time-of-check to time-of-use)问题。
- 比如先检查
if self.__class__.flag is False:再设为True,不锁就会漏判 - 对列表类变量做
append()、pop()、remove(),同样要锁住整条语句 - 推荐用
with self.__class__.lock:,避免异常时锁未释放 - 示例:
import threading
<p>class Counter:
counter = 0
lock = threading.Lock() # ✅ 类变量锁</p><pre class="brush:python;toolbar:false;">@classmethod
def increment(cls):
with cls.lock: # ✅ 锁住整个操作
cls.counter += 1
@classmethod
def set_if_zero(cls, value):
with cls.lock: # ✅ “检查+赋值”必须原子
if cls.counter == 0:
cls.counter = value
比 Lock 更轻量的替代方案有哪些?
不是所有场景都得用 threading.Lock。选错工具反而增加复杂度或掩盖问题。
- 仅计数:优先用
threading.atomic.AtomicInt(Python 3.10+),硬件级原子,无锁开销 - 需要队列式通信:用
queue.Queue替代类变量存任务,它线程安全但不支持随机访问 - 每个线程只读写自己专属数据:直接用
threading.local(),彻底避开共享 - 频繁读、极少写:可考虑
RWLock(需第三方库如readwrite_lock),但标准库不提供
最容易被忽略的是:类变量一旦被多线程修改,它的生命周期就脱离了单线程语义——哪怕你只在初始化阶段写一次,只要存在并发写路径,就必须按并发规则处理。锁不是可选项,是前提条件。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











