GIL保证字节码级别的原子性和线程安全性,因此当个字节码执行一定是安全的,执行结果一定是一致的。
而有些操作,底层需要通过多个字节码来完成,这样的操作就不是原子的,因此不是线程安全的。举个例子,a+=1 。反编译这个语句,发现它由4个字节码组成:>>> dis.dis(compile('a+=1', '', 'exec'))
1 0 LOAD_NAME 0 (a)
2 LOAD_CONST 0 (1)
4 INPLACE_ADD
6 STORE_NAME 0 (a)
8 LOAD_CONST 1 (None)
10 RETURN_VALUE
这个简单的语句,背后需要 4 个字节码协作完成:LOAD_NAME 将 a 当前的值加载进运行栈;
LOAD_CONST 将常量 1 加载到运行栈;
INPLACE_ADD 对栈上两个操作数进行加法运算;
STORE_NAME 将计算结果保存;
如果你学过汇编的话,你会发现Python字节码跟汇编指令非常像!GIL保证当个字节码的执行不会受到其他线程的任何干扰,但是任何字节码间都可能发生线程切换。
假设两个线程同时自增变量a,a当前值为0;线程A执行到第3步,自增结果1已算出,但未保存;这时线程B得到调度开始执行,同样算出结果1并抢先保存了;A回过头来将结果1保存,B的结果被覆盖了,最终a的值是1。然而,两个线程对a自增,它的值讲道理应该是2!这就是并发操作产生的竞争态,解决方法是用一个锁将这几个字节码作为原子操作保护起来。
我写过一个关于Python虚拟机内部实现的专栏Python源码深度剖析,里面有些许介绍,有兴趣可以看看~