什么是GIL
GIL是python的全局解释器锁,他是一个互斥锁,由于GIL的存在在同一时间只能有一个线程持有对python解释器的控制权。
GIL产生的原因
python是使用引用计数的方式管理内存(GC),python在内存中使用的数据会被添加一个计数,使用某个数据的话该数据的引用计数就会+1释放数据的话计数-1,当计数为0时会被GC清除掉。而GC本质也是一传代码,执行也会需要一个线程。这两个线程同时可以操作一段数据很可能会导致泄漏的内存永远不会释放,而没有被释放的内存依然可以被引用就会产生很多错误。
通过上图可以看出,GC与其他线程都在竞争解释器的执行权,而CPU何时切换,以及切换到哪个线程都是无法预支的,这样一来就造成了竞争问题,假设线程1正在定义变量a=10,而定义变量第一步会先到到内存中申请空间把10存进去,第二步将10的内存地址与变量名a进行绑定,如果在执行完第一步后,CPU切换到了GC线程,GC线程发现10的地址引用计数为0则将其当成垃圾进行了清理,等CPU再次切换到线程1时,刚刚保存的数据10已经被清理掉了,导致无法正常定义变量。
综上所述可以通过添加互斥锁来保证两个线程不会同时操作一段数据。
但是添加锁就有可能会导致死锁,而且重复的获取和释放锁也会使程序的性能下降,而GIL是解释器本身的一个锁,任何代码执行时都需要获得这个锁,这样就可以防止死锁的情况但是这么做同时也把多线程程序变成了单线程,是的程序整体的性能降低。
GIL的加锁与解锁时机
加锁的时机:在调用解释器时立即加锁
解锁时机:
-
当前线程遇到了IO时释放
-
当前线程执行时间超过设定值时释放
如何减小GIL造成的影响
可以使用开启多进程的方法,每个python进程都有自己的python解释器和独立的内存空间
用多线程运算
def task():
a=0
for i in range(1000000000):
a+=1
if __name__ == '__main__':
start=time.time()
for i in range(5):
p=Thread(target=task)
p.start()
print(time.time()-start)
结果为:0.15290188789367676
使用进程来处理
def task():
a=0
for i in range(1000000000):
a+=1
if __name__ == '__main__':
start=time.time()
for i in range(5):
p=Process(target=task)
p.start()
print(time.time()-start)
结果为:0.018976926803588867
可见对于运算密集型的程序多进程要比多线程处理速度快了十多倍
自定义的线程锁与GIL的区别
GIL保护的是解释器级别的数据安全,比如对象的引用计数,垃圾分代数据等等,具体参考垃圾回收机制详解。
对于程序中自己定义的数据则没有任何的保护效果,这一点在没有介绍GIL前我们就已经知道了,所以当程序中出现了共享自定义的数据时就要自己加锁,如下例:
from threading import Thread,Lock import time a = 0 def task(): global a temp = a time.sleep(0.01) a = temp + 1 t1 = Thread(target=task) t2 = Thread(target=task) t1.start() t2.start() t1.join() t2.join() print(a)
过程分析:
1.线程1获得CPU执行权,并获取GIL锁执行代码 ,得到a的值为0后进入睡眠,释放CPU并释放GIL
2.线程2获得CPU执行权,并获取GIL锁执行代码 ,得到a的值为0后进入睡眠,释放CPU并释放GIL
3.线程1睡醒后获得CPU执行权,并获取GIL执行代码 ,将temp的值0+1后赋给a,执行完毕释放CPU并释放GIL
4.线程2睡醒后获得CPU执行权,并获取GIL执行代码 ,将temp的值0+1后赋给a,执行完毕释放CPU并释放GIL,最后a的值也就是1
之所以出现问题是因为两个线程在并发的执行同一段代码,解决方案就是加锁!
from threading import Thread,Lock import time lock = Lock() a = 0 def task(): global a lock.acquire() temp = a time.sleep(0.01) a = temp + 1 lock.release() t1 = Thread(target=task) t2 = Thread(target=task) t1.start() t2.start() t1.join() t2.join() print(a)
过程分析:
1.线程1获得CPU执行权,并获取GIL锁执行代码 ,得到a的值为0后进入睡眠,释放CPU并释放GIL,不释放lock
2.线程2获得CPU执行权,并获取GIL锁,尝试获取lock失败,无法执行,释放CPU并释放GIL
3.线程1睡醒后获得CPU执行权,并获取GIL继续执行代码 ,将temp的值0+1后赋给a,执行完毕释放CPU释放GIL,释放lock,此时a的值为1
4.线程2获得CPU执行权,获取GIL锁,尝试获取lock成功,执行代码,得到a的值为1后进入睡眠,释放CPU并释放GIL,不释放lock
5.线程2睡醒后获得CPU执行权,获取GIL继续执行代码 ,将temp的值1+1后赋给a,执行完毕释放CPU释放GIL,释放lock,此时a的值为2