Python中哪些操作是线程安全的?为何stats["ok"] = stats["ok"] +1这类多字节码操作未出现竞态条件?
这是个特别棒的问题,刚好戳中了CPython里GIL、字节码执行和线程调度之间最容易混淆的几个点,我来一步步给你理清楚:
先澄清你对GIL的理解偏差
首先你之前的第一条理解有个小误差:GIL保证的是同一时刻只有一个线程在执行Python字节码,不是VM的底层指令。而且GIL并不是全程锁死的——它会在特定时机主动释放,比如:
- 线程执行完指定数量的字节码(默认是1000条,可以用
sys.getcheckinterval()查看) - 线程遇到I/O阻塞(比如sleep、文件读写、网络请求)
- 线程主动调用
time.sleep(0)让出GIL
只有单个字节码指令的执行过程是绝对不会被打断的,因为GIL会在字节码执行完成后才会检查是否需要切换线程。
为什么你的inc函数一开始没出现竞态?
你猜的第三点完全正确!核心原因就是:这个函数的字节码序列太短了,执行速度快到离谱,线程调度器根本没来得及抢占GIL,整个操作就已经跑完了。
你的inc函数总共才18条字节码,远低于默认1000条的字节码切换阈值。线程在执行完整个函数的所有字节码之前,GIL不会被主动释放,其他线程完全没机会插进来执行中间步骤(比如读取stats["ok"]之后、赋值之前)。所以看起来结果是“一致”的,但这只是巧合,不是真正的线程安全。
你后来的实验也验证了这一点:当你拆分步骤并加入sleep(sleep会触发GIL释放),或者把字节码执行量加大(比如在函数里加个循环跑几百次),甚至手动把切换阈值改小(sys.setcheckinterval(1)),就能立刻看到竞态条件出现——多个线程会同时读取到旧值,然后都加1再写回去,导致最终结果比预期小。
到底哪些操作才是真正线程安全的?
CPython官方定义的“线程安全”原子操作,是指那些能被编译成单个字节码指令的操作,这些操作的执行过程不会被线程切换打断。常见的例子包括:
- 对列表的
append()操作(对应LIST_APPEND字节码) - 对字典的单个键值对直接赋值(比如
stats["ok"] = 5,对应STORE_SUBSCR字节码) - 简单的变量赋值(比如
x = 10,对应STORE_FAST) - 对集合的
add()操作(对应SET_ADD字节码)
而像stats["ok"] = stats["ok"] +1、x +=1这类看似简单的一行代码,本质上是多个字节码指令的组合(读取旧值→计算新值→写入新值),属于非原子操作,本身是线程不安全的——只是在某些场景下(比如字节码太少)没被触发竞态而已。
再补充GIL和内置类型的关系
你看到的官方文档里说GIL简化了内置类型(比如dict)的实现,意思是:因为GIL保证了同一时刻只有一个线程在操作Python对象,所以内置类型的内部结构(比如dict的哈希表)不需要额外加锁来保护多线程并发修改。但这并不意味着所有涉及内置类型的操作都是原子的——只有单个字节码级别的操作才是安全的,多字节码的组合操作依然有竞态风险。
备注:内容来源于stack exchange,提问作者Vladyslav Chaikovskyi

