一、全局解释器锁
GIL是CPython解释器中的线程全局锁,今天我们来说一说它。GIL由于历史原因而存在,在之后的CPython中也应该会继续存在下去。
GIL能够保证解释器进程中同时仅有一个线程执行,不允许多线程并行执行,简化了内存管理等底层细节。但它导致了Python多线程程序的执行效率问题,在多核CPU环境下和计算密集型任务中这个问题尤其严重。
Python的线程就是操作系统的线程(POSIX thread || Win thread),线程调度也直接使用操作系统的线程调度。
二、GIL原理
运行中的线程持有GIL,当线程遇到I/O操作时,释放GIL。如果是CPU密集型任务,则在一定间隔后进行检查,默认是100ticks,其中一个tick是若干条Python解释器指令。
Python提供了一种用于线程同步的锁,它不是简单的互斥锁,而是由互斥锁和条件变量构成的二进制信号量。GIL是该锁的一个实例。了解条件变量相关戳我~GIL的释放和持有请求过程如下伪代码所示:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17release(){
mutex.acquire()//互斥锁
locked = 0 //状态
mutex.release()
cond.signal()//通知条件变量队列中第一个线程,进入操作系统Ready队列
}
//持有GIL过程
acquire(){
mutex.acquire()
whiled(locked){
cond.wait(mutex)//进入条件变量等待队列
}
locked = 1
mutex.release()
}
对于CPU密集型的多线程应用,在单核和多核环境下利用修改Python源码的方式测试其执行效率note1. 来自UnderstandingGIL.pdf↩
单核环境下,它具有不错的性能,线程交替的执行,并且线程切换的频率较低。
但是多核环境下,就会产生灾难性的后果。运行中的线程1在执行一定指令后唤醒条件变量队列中的等待线程2,但是由于两个线程都处于Ready状态,因此操作系统调度后可能仍然运行线程1。这个过程可能反复进行数百次,时间开销就会很大。
即使是I/O密集型的应用,由于缓冲区的存在,I/O操作可能不会阻塞,但是却要反复释放GIL造成GIL颠簸,开销也不小。
三、消除GIL的努力
Python1.5曾有Patch努力消除GIL,但是由于降低了单线程的执行效率、对两个以上的线程效果差、Python大量的库需要重写等原因,该方案最后没有成功。下面的链接是Python的作者Guido对去除GIL的态度。
四、改进GIL
Python3.2之后有了新的GIL机制(2009年byAntoine Pitrou),主要的目的是减轻GIL的颠簸,在这里Reworking the GIL有对这些改进的阐述。当然下面也有。
不使用tick作为计算密集型线程的释放计量,使用全局变量gil_drop_request。它的初始值为0,运行的线程若不主动释放GIL则一直运行到该变量值变为1。这样线程切换的时间更平稳更加可预计。
等待队列中的线程睡眠一个时间(默认5ms),若该时间间隔内运行线程不主动释放GIL,则将变量设为1并继续睡眠,这样运行线程将进入暂停状态(避免该线程被立刻调度运行),并引发一次操作系统的线程调度(继续运行的并不一定是提出timeout的线程)。如下图所示:
但是,新的GIL也带来了诸如响应时间长等问题(护航效应),比如I/O密集线程释放GIL后,一个计算密集线程获取GIL,那么I/O线程至少需要等待5ms才能再次运行,再次运行5ms后又要释放GIL,这样响应时间就会很长。比如这里给出的这个实验代码GIL-with-priorities。但是这里有一个疑问,这里说明这个带有线程优先级的GIL在python3.2开发了,为什么我的环境Python3.4在执行时双线程反而效率降低,这个问题留待探究。
五、避免GIL带来的缺陷
也存在几种现有方案来避免GIL的缺陷。当然,这些替代方案也有它们各自的问题。1.使用multiprocessing替代Thread,多进程通信和切换开销较大
2.用其他的解释器实现,比如Jython或者PyPy,可用的库少
试验
在4核i5-4210U处理器上进行了简单测试,使用Python3.4,结果说明新的GIL解决了GIL原有的一些效率问题:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38"""testgil"""
import time
from threading import Thread
def (func):
"""time it decorator"""
def new_func(*args, **args2):
"""wrapper"""
t_0 = time.time()
print("@%s, {%s} start" % (time.strftime("%X", time.localtime()), func.__name__))
back = func(*args, **args2)
print("@%s, {%s} end" % (time.strftime("%X", time.localtime()), func.__name__))
print("@%.3fs taken for {%s}" % (time.time() - t_0, func.__name__))
return back
return new_func
def count(num):
"""count func"""
while num > 0:
num -= 1
def multi_thread():
"""multi thread test"""
t_1 = Thread(target=count, args=(100000000,))
t_1.start()
t_2 = Thread(target=count, args=(100000000,))
t_2.start()
def single_thread():
"""single thread test"""
t_1 = Thread(target=count, args=(200000000,))
t_1.start()
if __name__ == '__main__':
# single_thread()
multi_thread()
测试结果如下:
程序时间单线程17.927s
双线程17.435s