GIL 目的在于保护 Python 虚拟机内部状态。举个例子,Python 很多变量空间,比如全局变量,内部是用 dict 来实现的。
变量的赋值,在 Python 内部最终是执行 STORE_NAME 字节码,这个字节码将变量的值,保存到对应的 dict 对象中。
假设这个动作底层是由字典的 dict.set(name, value)函数负责,它会非常复杂,还涉及 dict 对象扩容缩容,肯定不是线程安全的。
那怎么办呢?①dict.set(name, value)加锁;②用 GIL 保证同一时间只有一个线程在执行字节码。
Python 选择②,因为①引入的线程开销也不小。
有测试表明①虽然提升了 Python 的并行能力,但获得的性能提升非常有限,单线程下则全是消耗。
那为什么有 GIL 之后,多线程应用还需要加锁呢?
举个例子,有个全局变量 a,多个进程并发执行 a += 1 。
这个语句编译后大概会生成这样几个字节码:
1 LOAD_NAME 将变量从名字空间 dict 中取出,并保存在临时栈;
2 ADD 在临时栈中做加法操作;
3 STORE_NAME 将计算结果保存到名字空间 dict 中;
GIL 保证了线程在执行一个字节码时,其他线程不能执行,以便保护名字空间 dict 的安全性。
但这 3 个字节码之间可以任意切换,这样应用就会产生中间态。
举个例子,线程①执行 LOAD_NAME 后,切到其他线程执行,变量 a 发生了修改。
线程①恢复执行后,它临时栈中的值仍是旧的,这样就会覆盖了其他线程的写入。
因此,需要用户自行加锁,保存 a += 1 对应的这几个字节码的原子性,一次性执行,中间不能被打断。
总而言之,GIL 保证一个线程在执行字节码时,其他线程不能同时执行,目的是保护虚拟机内部状态的线程安全性;
用户自己加的锁,是为了让多条字节码成为一个原子操作,中间不会发生线程切换,目的在于保护程序逻辑的正确性,消除竞争态。
想要完全理解这个问题,需要了解 Python [内建对象] [虚拟机] [字节码] 等知识,有兴趣的话推荐看一个叫 [Python 源码剖析] 的专栏:
https://fasionchan.com/python-source/virtual-machine/gil/