平凡

python的PIL

2018/09/29 Share

英文原文:
https://realpython.com/python-gil/

Python全局解释器锁(Python Global Interpreter Lock or GIL),是为了在任何时刻只能让一个线程拿到python解释器的执行,即只有一个线程可以运行。这对于运行单线程程序的机器没什么影响,但对于多线程程序简直就是灾难。你电脑是多核的?对不起,不可以运行多线程。所以GILPython一个臭名昭著的特性。

GIL为python解决了哪些问题

python的内存管理使用计数引用,python中的每个对象都会有一个计数器,来表示有几个引用指向该对象。当这个计数器为0时,对象占用的内存就被释放掉。

让我们看下计数器具体是怎么工作的:

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

在上面例子中,空列表[]计数引用为3,这是因为有三个变量引用了它:a, ‘b’, 传入sys.getrefcount(a)的a(这倒有点意思)

这和GIL有什么关系呢?

当有两个线程同时对这个计数引用进行增加和减少操作时,这个计数引用机制就需要被保护。如果这个值比预期的要大,那么所以的内存就不会被释放,从而造成内存泄露。如果这个值比预期的值要小,后果更严重,会提前释放掉内存,这个时候如果有变量来访问它,就会引起程序崩溃。

如果对使用这个计数引用的所有线程加上锁,就不会出现不一致的情况。

但对多个对象加锁,可以会出现死锁问题。另一个问题是不断的获取与释放锁,会对系统的性能造成影响。

GIL是解释器的单锁——任何代码执行前都需要获取到这个锁。这样就防止了死锁(因为只有这一个锁,令人窒息的操作)。但它使得cpu密集型的python程序退化成了单线程程序。

Ruby也使用GIL来解决这个问题,但其他语言为了避免使用GIL带来的负面效应,干脆不使用计数引用机制了,使用垃圾回收(GC)来管理内存。


为什么python选择使用GIL

为什么python使用看起来这么low的方式呢?据Larry Hastings介绍,正是由于GIL,python才得以如此流行。在操作系统还没有出现线程的时代,python就已经开始发展了。python的设计目标就是为了简单、让开发者更快速,所以越来越多人在使用它。GIL实现简单,它比其他单线程程序运行更快(因为只有一个锁需要管理)。

正是由于GIL的特性,越来越多的c库(不管是线程安全还是线程不安全的)都被集成到python中。所以,GIL是一个实用主义的解决方案。

对多线程程序的影响

看一个CPU密集型的程序:

# single_threaded.py
import time
from threading import Thread

COUNT = 50000000

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

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

print('Time taken in seconds -', end - start)

在我的机器上,运行时间是0.38秒

# multi_threaded.py
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('Time taken in seconds -', end - start)
# multi_threaded.py
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('Time taken in seconds -', end - start)

现在使用多线程运行上面的程序,时间竟然惊人的达到了0.39秒!!!!


emmm,两个程序的运行时间如此相近,说明多线程是不可能多线程的了。GIL的存在,阻止了上面程序的并行运行

IO密集型程序,GIL对其影响不大


为什么还不把GIL移除

为什么移除不了?许多开发者早就想干这种事了,但那么多C库都利用了GIL,想把人一脚踢开,现实么?

现在也出现了一些其他的解决方案,但这些方案,要么太过复杂,要么牺牲了单线程的性能。

为什么python3 没移除它

如果将GIL从python3中移除,那么它运行单线程的性能会比python2低。如果被python2的支持者知道了这件事,不得笑死,所以你懂的……

怎么应对GIL的缺点

多线程VS多进程:最常见的解决方案是使用多进程,不使用多线程。每个python进程都会有自己的解释器和自己的内存空间,所以GIL就不是个问题了。python有个multiprocessing模块,方便我们创建进程(以前常用这个写爬虫):

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('Time taken in seconds -', end - start)

在我的机器上跑,只用了0.20秒,可见速度提升了将近两倍。

但多进程的性能开销是巨大的,所以提升的性能比两倍要低。


换一个解释器:python有多种解释器:CPython, Jython, IronPython ,PyPy,对应使用C, Java, C#, Python编写。只有CPython才有GIL

等、耗着、拖:等python大佬们把GIL彻底移除的那一天——那一天,所有pythoner们都激动的睡不着觉。现在已经有一些尝试:https://github.com/larryhastings/gilectomy


总结

Python的GIL机制很令人难受,但如果你不写多线程程序,就不会被影响(这逻辑没毛病)。

发表日期: September 29th 2018

版权声明: 本文采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可

CATALOG
  1. 1. GIL为python解决了哪些问题
  2. 2. 为什么python选择使用GIL
  3. 3. 对多线程程序的影响
  4. 4. 为什么还不把GIL移除
  5. 5. 为什么python3 没移除它
  6. 6. 怎么应对GIL的缺点
  7. 7. 总结