类变量被多个线程同时写入时不一定崩溃但一定存在竞态风险:自增等复合操作会丢失更新,赋值虽不崩溃却可能导致逻辑错误;必须用类级别Lock保护临界区,或用threading.local()实现线程隔离。

类变量被多个线程同时写入时一定会出问题
Python 中的类变量(如 MyClass.counter = 0)本质上是存储在类的 __dict__ 中的共享对象。一旦多个线程同时执行类似 MyClass.counter += 1 这样的操作,就不是原子行为——它实际拆成「读取、计算、写回」三步,中间可能被其他线程打断,导致丢失更新。哪怕只是赋值 MyClass.flag = True,如果多个线程写不同值,最终结果也取决于谁最后写,但至少不会引发崩溃;而自增/自减/列表 .append() 等复合操作,大概率产生数据错误。
threading.Lock 是最直接可控的保护方式
对类变量的读写临界区加锁,是最清晰、副作用最小的做法。注意锁本身也得是类级别的共享对象,不能每次新建:
import threadingclass Counter: count = 0 _lock = threading.Lock() # 类变量,所有实例和线程共用
@classmethod def increment(cls): with cls._lock: cls.count += 1
- 不要把
_lock放在方法里创建,否则锁无效 - 避免在锁内做耗时操作(如网络请求、文件读写),否则严重拖慢并发性能
- 如果只读不写,通常不需要锁;但若“读”依赖于多个类变量的一致状态(比如
cls.min_val和cls.max_val需同步读),仍需加锁
用 threading.local() 是另一种思路,但解决的是不同问题
threading.local() 创建的是线程局部副本,适用于「每个线程需要自己独立的类变量视图」的场景,比如请求上下文、数据库连接句柄等。它不保护共享修改,而是绕过共享:
import threadingclass RequestContext: _local = threading.local()
@classmethod def set_user_id(cls, uid): cls._local.user_id = uid # 每个线程写自己的副本 @classmethod def get_user_id(cls): return getattr(cls._local, 'user_id', None)
- 这不能替代锁来保护真正的共享计数器或开关标志
- 局部变量不会自动继承,子线程需手动设置
- 容易误以为“用了 local 就线程安全了”,其实只是隔离了访问路径
CPython 的 GIL 不足以保护类变量复合操作
GIL 只保证单个字节码指令的原子性,而 +=、.append()、del cls.cache[key] 等都会编译成多条字节码。你可以用 dis.dis(lambda: MyClass.counter += 1) 看到 LOAD_ATTR → INPLACE_ADD → STORE_ATTR 三步。GIL 在每条字节码间都可能释放并切换线程,所以必须靠显式锁来围住整个逻辑块。
真正容易被忽略的是:即使你只用类变量做标记(如 is_running = False),若多个线程同时设为 True 或 False,虽不报错,但语义上可能已混乱——比如本该只允许一个激活实例,却因竞态导致多个线程都通过了 if not cls.is_running: 判断。这种逻辑漏洞比数值错误更难排查。










