为何无IO的Ruby代码在GVL下仍出现竞态条件?
GVL下仍出现竞态条件的原因分析
你的核心误解是对GVL释放时机的认知偏差:Ruby的GVL并非只有在IO操作时才会释放,原生线程在执行完固定数量的Ruby字节码指令后,会主动释放GVL,让其他等待的线程有机会获取锁并执行代码——这就是无IO代码仍出现竞态的关键原因。
具体代码的竞态拆解
你的代码里存在两处致命的非原子操作,结合GVL的主动释放机制,就会导致to的数值超出预期:
while from > 0的判断与后续操作非原子
比如线程A执行while from > 0时,from的值为1,判断结果为真,但此时线程A刚好执行完预设的指令数,主动释放了GVL。线程B此时获取GVL,同样判断from > 0(此时from还是1),接着执行from -=1(from变为0)和to +=1。随后线程A重新获取GVL,继续执行剩下的from -=1(from变为-1)和to +=1——这就导致to多增加了1,多次触发后最终数值就会超过初始的100000000。from -=1和to +=1本身非原子
这两个操作在Ruby字节码中会被拆分为三个步骤:读取变量当前值→执行加减运算→将结果写回变量。如果在读取和写回之间发生GVL切换,同样会导致多个线程对同一变量的操作相互覆盖,进一步加剧竞态问题。
为什么加Mutex能解决问题
Mutex会把while循环内的所有操作包裹成一个原子执行块:只有获取到Mutex锁的线程才能执行这段代码,其他线程必须等待锁释放后才能进入。这就彻底规避了GVL主动释放带来的线程切换,保证了判断、自减、自增操作的原子性,因此to的数值始终等于初始的100000000。
补充:GVL的核心特性
GVL的作用是限制同一时间只有一个原生线程执行Ruby代码,但它是基于指令计数的可抢占式锁:
- 线程在执行IO操作、sleep、调用C扩展(如果C扩展主动释放GVL)时会释放GVL
- 即使没有上述操作,线程执行完固定数量的字节码指令后,也会主动释放GVL,让其他线程有运行机会
这意味着,只要代码中存在非原子的共享变量操作,即使没有IO,也可能因为GVL的主动切换出现竞态条件。
内容的提问来源于stack exchange,提问作者S0i
相关产品推荐
相关产品推荐

