GIL (Global Interperter Lock) 称作全局解释器锁,也可以称它为全局排他锁。GIL只在CPython中存在。比如JPython就没有这个概念。
在python多线程的情况下,每个线程的执行方式是这样的
获取GIL -> 执行代码,直到遇到IO操作,执行了一定的代码量(python2),执行了一定的时间(python3) -> 释放GIL
那么问题来了,GIL是全局锁,也就是一个进程中只有一个,没有拿到GIL就不允许在CPU中执行,所以Python多线程在并不能充分利用多核CPU。
CPU密集型任务的主要特点就是需要进行大量的计算,消耗CPU资源。CPU密集型程序虽然可以采用多任务来完成,但任务数越多花在任务切换的时间也就越多,所以要高效的利用CPU,CPU密集型任务的数量在理想的情况下应该等于CPU核心数+1。
多任务可以使用多进程和多线程来实现,在多进程情况下,进程数应该等于CPU核心数+1,多线程情况下,线程数也应该等于CPU核心数+1。Python在多进程下没有什么问题,但是在多线程下问题就出来了。
由于Python的GIL的存在,一个进程中只能有一个线程在执行,无法充分利用多核CPU。所以Python的多线程在处理CPU密集型任务时并不是很友好,解决的办法就是使用多进程+协程的方式。
IO密集型任务的主要特点就是大量涉及到网络、磁盘IO的任务。这类任务对CPU的消耗很低,任务的大部分时间都在等待IO操作完成,对于IO密集型任务,任务数越多,CPU的效率就越高,但是也有一个限度。比较理想的任务数是CPU核心数/(1-阻塞系数),阻塞系数一般在0.8-0.9,所以任务数应为核心数的5倍或10倍。
Python的线程会在遇到IO操作时释放GIL,执行别的线程, 所以Python多线程对IO密集型任务比较友好。
Python虽然是有GIL,但并不一定就是线程安全的。Python在两种情况下会释放线程锁,第一种,该线程在进入IO操作之前,会主动释放GIL;第二种,解释器不间断的执行了1000字节码(Python2)或运行15ms后(python3)后也会释放GIL。所以一个线程会随时释放GIL,那么它就不是线程安全的。
先看第一种情况,假设线程A进入了IO操作时释放了线程锁,由于A的IO操作等待时间不确定,所以线程B一定会得到GIL,这种情况下是线程安全的。
第二种情况,线程A在不间断的执行了1000字节码(Python2)或运行15ms后(python3)后释放了GIL,这个时候线程A会和线程B同时竞争GIL,Python3会提高B的优先级,但是也并不是说B一定会得到GIL。在这种情况下释放GIL,会出现线程安全问题。
这个首先要看一下Python执行程序的方式,Python在执行一个程序之前,会先将程序转换为字节码,然后解释器会去执行字节码。
是否线程安全就会取决于释放GIL前的操作是否是一个原子性的操作,或者说,释放GIL之前是否将一条Python语句执行完了。
举个例子
import dis
n = 0
def add():
global n
n = n + 1
return n
if __name__ == '__main__':
dis.dis(add)
执行这段代码会输出以下信息
9 0 LOAD_GLOBAL 0 (n)
2 LOAD_CONST 1 (1)
4 BINARY_ADD
6 STORE_GLOBAL 0 (n)
10 8 LOAD_GLOBAL 0 (n)
10 RETURN_VALUE
可以看到,n = n + 1
这条语句,会被翻译成4条字节码,所以加法这个操作并不是原子性的。如果在释放GIL的时候,刚好执行到了4 BINARY_ADD
这里,还未将计算后的值赋值给n
,那么在另外一个线程中执行的加法操作就会被这次的操作覆盖掉。这就是线程不安全的情况。