python的GIL锁(非常详细)

在了解python异步IO的过程中,接触到了python的GIL锁,找了许多国内外的资料,阅读完后发现较于其他博客中的说法有了更深的一些了解,因此写了这篇博客来进行记录,本文中的部分内容引用自real python,感觉是讲得最好的一份资料。

https://realpython.com/python-gil/

一、GIL锁简介

简而言之,Python全局解释器锁或GIL是一种互斥锁(或锁),仅允许一个线程持有Python解释器的控制权。

这意味着在任何时间点只能有一个线程处于执行状态。对于执行单线程程序的开发人员而言,GIL的影响并不明显,但它可能是CPU绑定和多线程代码的性能瓶颈。

二、python中为何会有GIL锁?

2.1 python的内存管理

首先让我们来看看python的内存管理,Python使用引用计数进行内存管理。这意味着用Python创建的对象具有引用计数变量,该变量跟踪指向该对象的引用数。当此计数达到零时,将释放对象占用的内存。

import sys
a = {}
b = a
c = b
print(sys.getrefcount(b))
>> 4

以上是一个小例子,字典对象分别被a,b,c,sys.getrefcount四个对象引用了,因此计数为4.
而既然使用了计数来进行区分,那就会导致多线程中遇到的非常多的问题:多个线程同时对一个值进行增加/减少的操作。如果发生这种情况,显然会导致非常严重的内存泄漏等问题。

2.2 GIL锁的产生

一般来说我们都是通过锁来解决内存方面的问题的,而python就是通过GIL锁来做这件事情。

但是既然有了锁,一个对象需要一个锁,那多个锁带来的死锁问题如何解决呢?重复获取或者释放锁带来的切换效率低的问题如何解决?因此,为了保证单线程情况下python的正常执行和效率,GIL锁(单一锁)由此产生了,由于只有一个,不会产生死锁且不用切换。

2.3不同语言中的GIL锁

只有cpython(使用C语言编写的PYTHON解释库,一般我们装python都是默认使用这个库,对C扩展的支持非常好)与ruby中有GIL锁,很大一部分原因就是这两种都是动态语言,类这种对象的内容是可以修改的,因此相对于JAVA等语言(使用区分得更加细的细粒度锁)来说,为了保证效率问题,就产生了GIL锁的问题。

三、GIL锁的影响

3.1 CPU限制程序

查看典型的Python程序(或与此相关的任何计算机程序)时,在性能上受CPU限制的程序与受I / O限制的程序之间是有区别:

受CPU限制的程序如下所示:

import time
from threading import Thread

COUNT = 50000000

def countdown(n):
    while n>0:
        n -= 1

t1 = Thread(target=countdown, args=(COUNT//2,))
t2 = Thread(target=countdown, args=(COUNT//2,))
start = time.time()
t1.start()
t2.start()
t1.join()
t2.join()
end = time.time()
print('Mul Time taken in seconds -', end - start)
t3 = countdown(COUNT)
endd = time.time()
print('Time taken in seconds -', endd - end)
>>Mul Time taken in seconds - 3.9715993404388428
>>Time taken in seconds - 4.038775682449341

可以看到,在这种是由于CPU的运算而导致运算时间长的问题,由于GIL锁的存在,多线程的运算速度和普通版本的运算速度几乎完全一样。

而在现在运用较多的IO密集型程序中,GIL锁的影响就没有那么大了(原因是在等待IO的过程中锁是可以被共享的),当然,IO密集情况下,使用协程进行异步IO可以进一步提升效率,这就是另外一说了,想要了解的可以通过另外一篇文章进行了解。

在加上3.0版本之后,Python内置了一种机制,该机制强制线程在固定的连续使用间隔后释放GIL ,如果没有其他人获得GIL,则同一线程可以继续使用它。

而在3.2版本之后,Antoine Pitrou添加了一种机制来查看被其他线程丢弃的GIL获取请求的数量,并且不允许当前线程在其他线程有机会运行之前重新获取GIL。
在这些修复之后,相对于对效率的提升以及对C扩展的支持,GIL锁带来的影响好像并没有想象中的那么大了。

3.2 图像化展示GIL锁的影响

http://cenalulu.github.io/python/gil-in-python/

该文中的这些图片能非常好地帮助大家理解。

为了直观的理解GIL对于多线程带来的性能影响,这里直接借用的一张测试结果图(见下图)。图中表示的是两个线程在双核CPU上得执行情况。两个线程均为CPU密集型运算线程。绿色部分表示该线程在运行,且在执行有用的计算,红色部分为线程被调度唤醒,但是无法获取GIL导致无法进行有效运算等待的时间。
在这里插入图片描述

由图可见,GIL的存在导致多线程无法很好的立即多核CPU的并发处理能力。

那么Python的IO密集型线程能否从多线程中受益呢?我们来看下面这张测试结果。颜色代表的含义和上图一致。白色部分表示IO线程处于等待。可见,当IO线程收到数据包引起终端切换后,仍然由于一个CPU密集型线程的存在,导致无法获取GIL锁,从而进行无尽的循环等待。

python的GIL锁(非常详细)_第1张图片

简单的总结下就是:Python的多线程在多核CPU上,只对于IO密集型计算产生正面效果;而当有至少有一个CPU密集型线程存在,那么多线程效率会由于GIL而大幅下降。

3.3 结论

因此,我们可以得到一个结论,在一般程序中(主要是IO密集型),GIL锁并不会带来太大的影响,除非进行包括进行数学计算的程序,例如矩阵乘法,搜索,图像处理或者超大数据量的运算等。

四、解决方案

这方面不进行过多拓展了,目前最好的方法就是使用多进程的方式(multiprocessing)来替代多线程(注意!!前提是需要进行受CPU运算速度限制的程序),如果有需要的话,也可以使用多进程+纤程进一步提升效率。

你可能感兴趣的:(python深入,python,多线程,编程语言)