为什么未加锁的Python多线程代码没有出现预期的竞态条件?
未观测到竞态条件的核心原因
两个关键机制叠加,导致你当前的测试代码很难复现自增操作的竞态问题:
- CPython全局解释器锁(GIL)的原生切换规则
同一个Python进程内,任意时刻只有一个线程能执行Python字节码。GIL的切换并非每执行一条指令就发生,默认情况下解释器会连续执行约100个字节码指令后,才会主动检查是否需要让出锁给等待的其他线程。
你代码中的a.val += 1看似是单步原子操作,实际会拆解为3条独立的字节码指令:- 读取
a.val的当前值到栈内存 - 对栈上的数值执行+1计算
- 将计算结果写回
a.val对应的内存地址
只有当GIL的线程切换恰好发生在这3条指令的中间时,才会触发真正的竞态:也就是一个线程刚读到共享变量的旧值、还没完成新值写回时,就被切走执行权;其他被调度的线程同样读到相同的旧值完成自增,最终两次自增操作的效果会互相覆盖,只产生一次自增的实际结果。
- 读取
- 当前测试的操作规模太小,根本没进入GIL的切换窗口
你每个线程只执行10次自增操作,算上线程函数启动、循环判断的全部字节码总量,远低于触发GIL主动切换的100条指令阈值。很多时候线程拿到GIL后,会一口气把自己的10次自增全部执行完毕,直到整个increment函数退出才会让出GIL,根本没机会在自增操作的三步流程中途被切走,自然每次运行都能得到完全正确的40这个结果。
如果你想稳定复现这类竞态,可以把每个线程的自增次数提升到10万级,让整体字节码执行量超过GIL的切换阈值;也可以在自增循环里加
time.sleep(0)主动触发GIL让出,强制把自增操作拆成可被打断的片段,最终会观测到最终结果远小于理论计算值。需要明确的是,GIL只能降低竞态的发生概率,完全不能替代显式锁来保证多线程下的共享变量修改安全。
内容的提问来源于stack exchange,提问作者Patrick Perkins
相关产品推荐
相关产品推荐

