python gil原理_探究python的GIL

一、全局解释器锁

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

参考

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值