Python 之GIL笔记

什么是GIL

GIL是Global Interpreter Lock的缩写,是一个互斥锁,它只允许一个线程控制Python解释器。也就说同一时间只能执行一个线程,即使是多个线程同时启动,其实不是并行执行。这个GIL对于单线程程序来说没有影响,对于多线程程序影响就比较大了,对于CPU密集型程序,启动多线程和不启动,性能没有什么不同,后面通过代码验证一下。

GIL在Python中的用途

众所周知,Python使用引用计数进行内存管理。在Python中创建的对象有一个引用计数变量,该变量跟踪指向该对象的引用的数量。当此计数达到零时,对象占用的内存将被释放。

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

像上面的例子,a和sys.getrefcount引用了两次,所以输出引用次数为2,如果直接sys.getrefcount([])就只有sys.getrefcount的一次引用,如果b=a,就会再算一次引用,所以sys.getrefcount(a)输出是3次引用,但是再次sys.getrefcount(a)为什么还是3呢?不是说sys.getrefcount也算一次引用吗?这是因为a作为变量传入sys.getrefcount时确实算一次引用,但这个函数执行完之后,这个引用也减掉了,所以多次执行这个函数都看到的是一样的。如果a=-5~256这些数字时,发现初始引用各种各样,因为在Python启动解释器的时候会创建一个小整数池,这些对象会被自动创建加载到内存中等待调用。
问题来了,这个引用计数变量需要保护,以避免两个线程同时增加或减少其值的竞争条件。如果发生这种情况,可能会导致从未释放的泄漏内存,或者更糟的是,在对该对象的引用仍然存在时错误地释放内存。这可能会导致Python程序崩溃。
这个引用计数变量可以通过向跨线程共享的所有数据结构添加锁来保持安全,这样它们就不会被不一致地修改。
但是向每个对象或对象组添加一个锁意味着将存在多个锁,这可能会导致另一个问题死锁(只有当存在多个锁时才会发生死锁)。另一个副作用是重复获取和释放锁会降低性能。
GIL是解释器本身的一个锁,它添加了一个规则,即执行任何Python字节码都需要获取解释器锁。这样可以防止死锁(因为只有一个锁),并且不会带来太多性能开销。

GIL 对多线程程序的影响

以cpu密集型程序为例,

#单线程
import time
COUNT = 50000000

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

start = time.time()
countdown(COUNT)
end = time.time()

print(end - start)

执行输出2.3860809803009033(cpu是i7)

#多线程
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(end - start)

执行输出2.2348098754882812,两个版本的完成时间几乎相同。在多线程版本中,GIL阻止线程并行执行。不过GIL对I/O密集型的多线程程序的性能没有太大影响,因为在线程等待I/O时,锁在线程之间共享。

怎么办

有一种简单的方案是使用多进程,每个Python进程都有自己的Python解释器和内存空间,因此GIL不会成为问题,例子如下。

from multiprocessing import Pool
import time

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

if __name__ == '__main__':
    pool = Pool(processes=2)
    start = time.time()
    r1 = pool.apply_async(countdown, [COUNT//2])
    r2 = pool.apply_async(countdown, [COUNT//2])
    pool.close()
    pool.join()
    end = time.time()
    print(end - start)

执行输出1.454556941986084,可以看到虽然执行速度有提升,但是并没有和预期一样提升一倍,因为多进程比多线程更重,性能消耗也更大。

参考

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

你可能感兴趣的:(学习笔记,python,多线程,多进程)