Python多线程另一个很重要的话题——GIL(Global Interpreter Lock,即全局解释器锁)鲜有人知,甚至连很多Python老司机都觉得GIL就是一个谜

一、一个不解之谜

耳听为虚,眼见为实。不妨看一个例子感受下GIL为什么会让人不明所以

下面这段很简单的cpu-bound代码:

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

现在,假设一个很大的数字n = 100000000,试试单线程的情况下执行CountDown(n),使用8核的MacBook上执行耗时为5.4s

这时,想要用多线程来加速,比如下面这几行操作:

from threading import Threadn = 100000000t1 = Thread(target=CountDown, args=[n // 2])
t2 = Thread(target=CountDown, args=[n // 2])
t1.start()
t2.start()
t1.join()
t2.join()

在同一台机器上运行,结果发现这不仅没有得到速度的提升,反而让运行变慢,总共花了9.6s

决定使用四个线程再试一次,结果发现运行时间还是9.8s,和2个线程的结果几乎一样

这是怎么回事呢?难道是这是一台假的8核MacBook?提出了下面两个猜想:

第一个怀疑:MacBook机器出问题了吗?

这不得不说是一个合理的猜想。因此找了一个单核CPU的台式机跑了一下上面的实验。发现在单核CPU电脑上,单线程运行需要11s时间,2 个线程运行也是11s时间
虽然不像第一台机器那样多线程反而比单线程更慢,但是这两次整体效果几乎一样

看起来并不像是电脑的问题,而是Python的线程失效导致没有起到并行计算的作用

第二个怀疑:Python的线程是不是假的线程?

Python 的线程的确封装了底层的操作系统线程,在Linux系统里是Pthread(全称为 POSIX Thread),在Windows系统里是Windows Thread
另外,Python的线程也完全受操作系统管理,比如协调何时执行、管理内存资源、管理中断等等

所以,虽然Python的线程和C++的线程本质上是不同的抽象,但它们的底层并没有什么不同

二、为什么有GIL

看来上面的两个怀疑都不能解释这个未解之谜,那究竟谁才是罪魁祸首呢?事实上,正是全局解释器锁GIL导致Python线程的性能并不像期望的那样

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

当然,CPython会做一些小把戏,轮流执行Python线程。这样一来用户看到的就是伪并行——Python线程在交错执行,来模拟真正并行的线程

那么,为什么CPython需要GIL呢?这其实和CPython的实现有关

CPython使用引用计数来管理内存,所有Python脚本中创建的实例都会有一个引用计数,来记录有多少个指针指向它。当引用计数只有0时则会自动释放内存

什么意思呢?看下面这个例子:

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

这个例子中,a的引用计数是3,因为有a、b和作为参数传递的getrefcount这三个地方,都引用了一个空列表

这样一来,如果有两个Python线程同时引用了a,就会造成引用计数的race condition,引用计数可能最终只增加 1,这样就会造成内存被污染
因为第一个线程结束时会把引用计数减少1,这时可能达到条件释放内存,当第二个线程再试图访问a时就找不到有效的内存

所以,CPython引进GIL其实主要就是这么两个原因:

  • 一是设计者为了规避类似于内存管理这样的复杂的竞争风险问题(race condition)
  • 二是CPython大量使用C语言库,但大部分C语言库都不是原生线程安全的(线程安全会降低性能和增加复杂度)

三、GIL是如何工作的

3.1 GIL的工作示例

下面这张图是一个GIL在Python程序的工作示例:

3.2 锁住GIL

其中,Thread1、2、3轮流执行,每一个线程在开始执行时都会锁住GIL,以阻止别的线程执行,同样的,每一个线程执行完一段后会释放GIL,以允许别的线程开始利用资源

3.3 释放GIL

可能会发现一个问题:为什么Python线程会去主动释放GIL呢?毕竟,如果仅仅是要求Python线程在开始执行时锁住GIL,而永远不去释放 GIL,那别的线程就都没有了运行的机会

没错,CPython中还有另一个机制叫做check_interval,意思是CPython解释器会去轮询检查线程GIL的锁住情况。每隔一段时间Python解释器就会强制当前线程去释放GIL,这样别的线程才能有执行的机会

不同版本Python中check interval的实现方式并不一样

  • 早期的Python是100个ticks,大致对应了1000个bytecodes
  • Python3以后,interval是15毫秒

当然,不必细究具体多久会强制释放GIL,这不应该成为程序设计的依赖条件。只需明白CPython解释器会在一个合理的时间范围内释放GIL即可


整体来说,每一个Python线程都是类似这样循环的封装,看下面这段代码:

for (;;) {if (--ticker < 0) {ticker = check_interval;/* Give another thread a chance */PyThread_release_lock(interpreter_lock);/* Other threads may run now */PyThread_acquire_lock(interpreter_lock, 1);}bytecode = *next_instr++;switch (bytecode) {/* execute the next instruction ... */ }
}

从这段代码中,可以看到每个Python线程都会先检查ticker计数,只有在ticker大于0的情况下,线程才会去执行自己的bytecode

四、Python的线程安全

有了GIL并不意味着Python编程者就不用去考虑线程安全

即使GIL仅允许一个Python线程执行,Python还有check interval这样的抢占机制。先看看这样一段代码:

import threadingn = 0def foo():global nn += 1threads = []
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这一句代码让线程并不安全。如果翻译foo这个函数的bytecode就会发现,它实际上由下面四行 bytecode 组成:

>>> import dis
>>> dis.dis(foo)
LOAD_GLOBAL              0 (n)
LOAD_CONST               1 (1)
INPLACE_ADD
STORE_GLOBAL             0 (n)

而这四行bytecode中间都是有可能被打断的

所以,千万别想着有了GIL就可以高枕无忧,仍然需要去注意线程安全

GIL的设计主要是为了方便CPython解释器层面的编写者,而不是Python应用层面的程序员。作为Python的使用者还是需要lock等工具来确保线程安全。比如下面的这个例子:

n = 0
lock = threading.Lock()def foo():global nwith lock:n += 1

五、如何绕过GIL

学到这里,估计有的Python使用者感觉自己像被废了武功一样,觉得降龙十八掌只剩下了一掌。其实大可不必,Python的GIL是通过CPython的解释器加的限制。如果代码并不需要CPython解释器来执行,就不再受GIL的限制

事实上,很多高性能应用场景都已经有大量的C实现的Python库,例如NumPy的矩阵运算就都是通过C来实现的,并不受GIL影响

所以,大部分应用情况下并不需要过多考虑GIL,因为如果多线程计算成为性能瓶颈,往往已经有Python库来解决这个问题

换句话说,如果应用真的对性能有超级严格的要求,比如100us就对应用有很大影响,那Python可能不是最优选择

总的来说,只需要重点记住绕过GIL的大致思路:

  1. 绕过CPython使用JPython(Java实现的Python解释器)等别的实现
  2. 把关键性能代码放到别的语言(一般是C++)中实现

六、思考题

  • 问题

在处理cpu-bound的任务时,为什么有时候使用多线程会比单线程还要慢些?

  • 答案

由于GIL采用轮流运行线程的机制,GIL需要在线程之间不断轮流进行切换,线程如果较多或运行时间较长,切换带来的性能损失可能会超过单线程

  • 问题

GIL是一个好的设计吗?事实上在Python 3之后,有很多关于GIL改进甚至是取消的讨论,你的看法是什么呢?

  • 答案

在python3中,GIL不使用ticks计数,改为使用计时器(执行时间达到阈值后interval=15毫秒,当前线程释放GIL),这样对CPU密集型程序更加友好,但依然没有解决GIL导致的同一时间只能执行一个线程的问题,所以效率依然不尽如人意

多核多线程比单核多线程更差,原因是单核下多线程,每次释放GIL唤醒的那个线程都能获取到GIL锁,所以能够无缝执行,但多核下,CPU0释放GIL后,其他CPU上的线程都会进行竞争,但GIL可能会马上又被CPU0拿到,导致其他几个CPU上被唤醒后的线程会醒着等待到切换时间后又进入待调度状态,这样会造成线程颠簸(thrashing)导致效率更低。

python下想要充分利用多核CPU就用多进程,原因是每个进程有各自独立的GIL,互不干扰,这样就可以真正意义上的并行执行,所以在python中,多进程的执行效率优于多线程(仅仅针对多核CPU而言)

GIL仍然是一种好的设计,虽然损失了一些性能,但在保证资源不发生冲突,预防死锁方面还是有一定作用的

【Python核心】全局解释器锁GIL相关推荐

  1. Python进阶并发基础--线程,全局解释器锁GIL由来,如何更好的利用Python线程,

    全局解释器锁GIL 官方对于线程的介绍: 在 CPython 中,由于存在全局解释器锁,同一时刻只有一个线程可以执行 Python代码(虽然某些性能导向的库可能会去除此限制).如果你想让你的应用更好地 ...

  2. Python培训教程:什么是Python全局解释器锁(GIL)?

    本期Python培训教程小编为大家带来的是关于"什么是Python全局解释器锁(GIL)?"的问题,全局解释器锁是计算机程序设计语言解释器用于同步线程的工具,使得在同一进程内任何时 ...

  3. c语言的锁和Python锁,Python中全局解释器锁、多线程和多进程

    全局解释器锁(GIL)只允许1个Python线程控制Python解释器.这也就意味着同一时间点只能有1个Python线程运行.如果你的Python程序只有一个线程,那么全局解释器锁可能对你的影响不大, ...

  4. Python全局解释器锁GIL与多线程

    Python中如果是 I/O密集型的操作,用多线程(协程Asyncio.线程Threading),如果I/O操作很慢,需要很多任务/线程协同操作,用Asyncio,如果需要有限数量的任务/线程,那么使 ...

  5. 问:为什么python中有了全局解释器锁GIL,还要有互斥锁?

    首先我们在进行对比之前,我们要知道什么是全局解释器锁,和什么是互斥锁,他们分别是用来做什么的才能解决这个问题. 首先介绍全局解释解释器锁GIL,Python代码的执行由Python 虚拟机(也叫解释器 ...

  6. python 全局解释器锁_python全局解释器锁(GIL)

    什么是全局解释器锁GIL 首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念.就好比C++是一套语言(语法)标准,但是可以用不同的编译 ...

  7. 【Python爬虫学习笔记11】Queue线程安全队列和GIL全局解释器锁

    Queue线程安全队列 在Python多线程编程中,虽然threading模块为我们提供了Lock类和Condition类借助锁机制来处理线程并发执行,但在实际开发中使用加锁和释放锁仍是一个经常性的且 ...

  8. python GIL 全局解释器锁详解

    Python多线程另一个很重要的话题--GIL(Global Interpreter Lock,即全局解释器锁)鲜有人知,甚至连很多Python老司机都觉得GIL就是一个谜 一.一个不解之谜 耳听为虚 ...

  9. python每隔30s检查一次_Python的全局解释器锁

    Python的全局解释器锁 全局解释器锁(GlobalInterpreter Lock,缩写GIL),是解释器同步线程的一种机制,它使得任何时刻仅有一个线程在执行. 即便在多核心处理器上,使用GIL  ...

最新文章

  1. Python 入门你要懂哪些?这篇文章总算讲清楚了
  2. NARF 特征点提取
  3. 实践自定义UI-ViewGroup
  4. 【项目管理】进度管理
  5. 【Linux系统编程】Linux文件操作
  6. luogu_1495【题解】中国剩余定理
  7. 95.91p30.space\/index.php,关于 ThinkPHP6 分页样式的定制及点击下一页搜索条件丢失的解决方法...
  8. 001 - JavaScript Array String
  9. [转] Android SDK manager 无法获取更新版本列表
  10. 再读红宝书(第四版)第二章 html 中的 javascript
  11. 建筑工程PPP项目财务风险控制探析
  12. Xmind如何添加水印
  13. 软件测试的模式(一、)
  14. C++和C#结构体转换的问题
  15. XML与java对象互转文档
  16. sd卡驱动分析之host
  17. 系统安全相关知识学习
  18. 基于MATLAB平台实现红绿灯(交通灯)识别
  19. USDP使用笔记(七)使用Flink1.14.3替换自带的老版Flink1.13
  20. 微信小程序 - 新闻动态 / 公告上下滚动列表(上下循环滚动,无限上下自动滚动列表)

热门文章

  1. EM算法解决二硬币问题(python)
  2. 0基础该怎么学习软件测试?
  3. 深度学习(自然语言处理)Seq2Seq学习笔记(采用GRU且进行信息压缩)(二)
  4. python实现mp3文件播放
  5. 金融科技大数据产品推荐:蓝金灵—基于大数据的电商企业供应链金融服务平台
  6. 计算时间差(精确到毫秒)(getTime()方法)
  7. 红外遥控38KHz载波,收发调制解码工作原理
  8. 急,有谁知道创业无息贷款的流程是怎样的?
  9. JAVA 面向对象基础知识
  10. Fatal: Failed to generate ABI binding: 5:9: expected ‘IDENT‘, found ‘.‘