目录
  • 1、什么是gil锁
  • 2、cpython对线程安全的内存管理机制
  • 3、gil锁的产生
  • 4、gil锁的底层原理
  • 5、python gil不能绝对保证线程安全
  • 6、总结

前言:

python的使用者都知道cpython解释器有一个弊端,真正执行时同一时间只会有一个线程执行,这是由于设计者当初设计的一个缺陷,里面有个叫gil锁的,但他到底是什么?我们只知道因为他导致python使用多线程执行时,其实一直是单线程,但是原理却不知道,那么接下来我们就认识一下gil锁

1、什么是gil锁

gil(global interpreter lock)不是python独有的特性,它只是在实现cpython(python解释器)时,引入的一个概念。

在官方网站中定义如下:

in cpython, the global interpreter lock, or gil, is a mutex that prevents multiple native threads from executing python bytecodes at once. this lock is necessary mainly because cpython’s memory management is not thread-safe. (however, since the gil exists, other features have grown to depend on the guarantees that it enforces.)

由定义可知,gil是一个互斥锁(mutex)。它阻止了多个线程同时执行python字节码,毫无疑问,这降低了执行效率。理解gil的必要性,需要了解cpython对于线程安全的内存管理机制。

2、cpython对线程安全的内存管理机制

python使用引用计数来进行内存管理,在python中创建的对象都会有引用计数,来记录有多少个指针指向它。当引用计数的值为0时,就会自动释放内存

我们来看一个小例子,来解释引用计数的原理:

>>> import sys
>>> a = []
>>> b = a
>>> sys.getrefcount(a)
3


可以看到,a 的引用计数值为 3,因为有 ab 和作为参数传递的 getrefcount 都引用了一个空列表。
如果有2个python线程同时引用a,那么2个线程都会尝试对其进行数据操作,多个线程同时对一个数据进行增加或减少的操作,如果发生这种情况,则可能导致内存泄漏

3、gil锁的产生

由于多个线程同时对数据进行操作,会引发数据不一致,导致内存泄漏,我们可以对其进行加锁,所以cpython就创建了gil

但是既然有了锁,一个对象就需要一把锁,那么多个对象就会有多把锁,可能会给我们带来2个问题

  • 1.死锁(线程之间互相争抢锁的资源)
  • 2.反复获取和释放锁而导致性能降低。

为了保证单线程情况下python的正常执行和效率,gil锁(单一锁)由此产生了,它添加了一个规则,即任何python字节码的执行都需要获取解释器锁。这样可以防止死锁(因为只有一个锁),并且不会带来太多的性能开销。但这实际上使所有受cpu约束的python程序(指的是cpu密集型程序)都是单线程的。

4、gil锁的底层原理

上面这张图,就是 gil python 程序的工作示例。其中,thread 123 轮流执行,每一个线程在开始执行时,都会锁住 gil,以阻止别的线程执行;同样的,每一个线程执行完一段后,会释放 gil,以允许别的线程开始利用资源。

线程释放gil锁有两种情况:

  • 一是遇到io操作
  • 二是time tick到期。io操作很好理解

比如发出一个http请求,等待响应。那么time tick到期是什么呢?time tick规定了线程的最长执行时间,超过时间后自动释放gil锁。python 3 以后,间隔时间大致为15毫秒
 

虽然都是释放gil锁,但这两种情况是不一样的。比如,thread1遇到io操作释放gil,由thread2和thread3来竞争这个gil锁,thread1不再参与这次竞争。如果是thread1因为time tick到期释放gil(多数是cpu密集型任务),那么三个线程可以同时竞争这把gil锁,可能出现thread1在竞争中胜出,再次执行的情况。单核cpu下,这种情况不算特别糟糕。因为只有1个cpu,所以cpu的利用率是很高的。

在多核cpu下,由于gil锁的全局特性,无法发挥多核的特性,gil锁会使得多线程任务的效率大大降低。

thread1在cpu1上运行,thread2在cpu2上运行。gil是全局的,cpu2上的thread2需要等待cpu1上的thread1让出gil锁,才有可能执行。如果在多次竞争中,thread1都胜出,thread2没有得到gil锁,意味着cpu2一直是闲置的,无法发挥多核的优势。

为了避免同一线程霸占cpu,在python3.2版本之后,线程会自动的调整自己的优先级,使得多线程任务执行效率更高。
既然gil降低了多核的效率,那保留它的目的是什么呢?这就和线程执行的安全有关。

5、python gil不能绝对保证线程安全

def add():
    global n
    for i in range(10**1000):
        n = n +1
def sub():
    global n
    for i in range(10**1000):
        n = n - 1
n = 0
import threading
a = threading.thread(target=add,)
b = threading.thread(target=sub,)
a.start()
b.start()
a.join()
b.join()
print n

上面的程序对n做了同样数量的加法和减法,那么n理论上是0。但运行程序,打印n,发现它不是0。问题出在哪里呢,问题在于python的每行代码不是原子化的操作。比如n = n+1这步,不是一次性执行的。如果去查看python编译后的字节码执行过程,可以看到如下结果。

19 load_global              1 (n)
22 load_const               3 (1)
25 binary_add          
26 store_global             1 (n)


从过程可以看出,n = n +1操作分成了四步完成。因此,n = n+1不是一个原子化操作。

  • 1.加载全局变量n
  • 2.加载常数1
  • 3.进行二进制加法运算
  • 4.将运算结果存入变量n。

根据前面的线程释放gil锁原则,线程a执行这四步的过程中,有可能会让出gil。如果这样,n=n+1的运算过程就被打乱了。最后的结果中,得到一个非零的n也就不足为奇。

6、总结

对于io密集型应用,多线程的应用和多进程应用区别不大。即便有gil存在,由于io操作会导致gil释放,其他线程能够获得执行权限。由于多线程的通讯成本低于多进程,因此偏向使用多线程。

对于计算密集型应用,由于cpu一直处于被占用状态,gil锁直到规定时间才会释放,然后才会切换状态,导致多线程处于绝对的劣势,此时可以采用多进程+协程。

到此这篇关于python 深入了解gil锁详细的文章就介绍到这了,更多相关python 深入了解gil锁内容请搜索www.887551.com以前的文章或继续浏览下面的相关文章希望大家以后多多支持www.887551.com!

参考资料:

https://realpython.com/python-gil/
https://zhuanlan.zhihu.com/p/97218985