python全局解释器锁_Python全局解释器锁(GIL)

Python全局解释器锁(GIL)

以如下代码为例:

1

2

3def CountDown(n):

while n > 0:

n -= 1

首先测试其在单线程情况下的运行时长,在我的机器上,上述代码的运行时间为6s左右。

下面测试上述代码的多线程版本的运行时间:

1

2

3

4

5

6

7

8

9

10from threading import Thread

n = 100000000

t1 = Thread(target=CountDown, args=[n // 2])

t2 = Thread(target=CountDown, args=[n // 2])

t1.start()

t2.start()

t1.join()

t2.join()

多线程版本的运行时间为9s左右。

从结果对比看,多线程反而比单线程耗时更久。那么,原因是什么?

实际上,正是Python中的全局解释器锁(GIL)导致这一问题的出现。

为什么会有GIL?

GIL是最流行的Python解释器CPython中的一个技术术语,本质上是类似操作系统的Mutex。每一个Python线程,在CPython解释器中运行时,都会先锁住自己的线程,阻止别的线程执行。

但是为了模仿并行,CPython会轮流执行Python线程,在多个线程之间不断切换。CPython这么做的原因在于Python的内存管理机制,CPython使用引用计数来管理内存,所有Python脚本中创建的实例,都会有一个引用计数来记录有多少个指针指向它。当引用计数为0时,则会自动释放内存。如下述代码所示:

1

2

3

4

5>>>import sys

>>>a = []

>>>b = a

>>>sys.getrefcount(a)

3

其中,a的引用计数为3,因为有a,b和作为参数传递的getrefcount三个指针指向该空列表。

那么,如果有两个Python线程同时引用了a,就会导致对引用计数的race condition,引用计数可能只会增加1,导致内存被污染。当第一个线程结束时,就会把引用计数减一,如果达到了内存释放条件,当第二个线程试图访问a时,就无法找到有效的内存。

因而,CPython引入GIL主要有以下两个原因:

设计者为了规避类似于内存管理这样的复杂的竞争风险问题(race condition);

因为CPython大量使用C语言库,大部分C语言库都不是原生线程安全的(线程安全会降低性能和增加复杂度)。

GIL的工作原理

如下图所示,Thread1、2、3轮流执行,当每一个线程开始执行时都会锁住GIL,以阻止别的线程执行;同样,当每一个线程执行完成后,会释放GIL,以允许别的线程开始利用资源。

但是如果一个线程一直锁住GIL,其他线程岂不是永远没有执行的机会?为了解决这个问题,CPython中还有另一个机制,check_interval。在程序运行过程中,CPython解释器会轮询检查线程GIL的锁住情况。每个一段时间,Python解释器会强制当前线程释放GIL,以给别的线程运行机会。

Python的线程安全

有了GIL并不意味着Python就一定是线程安全的。即使GIL仅允许一个Python线程执行,但是由于check interval抢占机制的存在,同样有可能发生竞争风险问题。以如下代码为例:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20import threading

n = 0

def foo():

global n

n += 1

threads = []

for i in range(100):

t = threading.Thread(target=foo)

threads.append(t)

for t in threads:

t.start()

for t in threads:

t.join()

print(n)

在上述代码中,大部分时间下,输出值都是100,但仍旧存在一些情况,输出值为99或98。

其中,n+=1这句代码会导致线程不安全。打印这句话的字节码:

1

2

3

4

5

6>>>import dis

>>>dis.dis(foo)

LOAD_GLOBAL 0 (n)

LOAD_CONST 1 (1)

INPLACE_ADD

STORE_GLOBAL 0 (n)

Check interval有可能发生在这四行代码的任何一句之后。

因而,在编写Python多线程程序时仍旧要注意线程安全问题。

GIL的设计主要是为了方便CPython解释器层面的编写者,而不是为了Python应用层面的程序员。在使用Python时,还是要使用lock等工具来确保线程安全。

1

2

3

4

5

6

7n = 0

lock = threading.Lock()

def foo():

global n

with lock:

n += 1

如何绕过GIL?

Python的GIL是通过CPython的解释器添加的限制,如果程序不需要CPython解释器来执行,就不受GIL的限制。因而,绕过GIL可以有以下两种方式:

不适用CPython解释器,转而使用别的解释器;

将关键性能代码使用其它语言实现。

思考题为什么开头的cpu-bound的程序,多线程会比单线程慢?

该程序不涉及I/O耗时环节,CPU都是被充分利用的。但是相比于单线程,多线程多了线程的切换,因而性能不如单线程。

参考

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值