目录

  • 一、Python 全局解释锁(GIL)是啥?
    • 1.1、GIL 解决了 Python 什么问题
    • 1.2、为什么选择了 GIL 这个解决方案
    • 1.3、对 Python 多线程的影响
    • 1.4、为什么 GIL 还没有被移除
    • 1.5、为什么不在 Python 3 中移除 GIL
    • 1.6、怎样处理 Python 的 GIL

一、Python 全局解释锁(GIL)是啥?

翻译自:What is the Python Global Interpreter Lock (GIL)?

Python 全局解释锁(Global Interpreter Lock,GIL)是一种互斥(或称为锁)的机制,在一个时间点下它只允许一个线程获取 Python 解释器的控制权。这意味着在一个时间点下只有一个线程在执行。对于单线程开发工作,GIL 的影响是看不到的,但是对于绑定 CPU 和多线程代码而言,GIL 将成为瓶颈。所以即使你的代码采用的是多线程机制,即使你的 CPU 是多核的,在任意时间点实际上仍只有一个线程在执行,这就是 Python 在多线程方面 “臭名昭著” 的原因,我们下面将探讨 GIL 对你 Python 代码的影响以及如何尽可能降低这种影响。

1.1、GIL 解决了 Python 什么问题

Python 使用引用计数进行内存管理,这意味着在 Python 中创建的每个对象都有一个引用计数变量,该变量标记指向该对象的引用数量。当计数达到零时,该对象占用的内存将被释放。我们通过一个简单的例子来演示一下:

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

在上面的例子中,空列表对象 [] 的引用计数是 3,分别被变量 ab 以及传递到 sys.getrefcount() 的参数所引用。

回到 GIL 问题上,由于这个引用计数变量需要保护,以避免两个具有竞争条件的线程同时增加或减少其值。如果发生这种情况,它可能导致内存泄露,内存永远不会释放,或者更糟糕的是,错误地释放内存,而该对象的引用却仍然存在,这可能会导致 Python 程序崩溃或其他“奇怪”的错误。(译者注:我们目前使用的基本都是 CPython 解释器,它的内存管理不是线程安全的,Jython 和 IronPython 就不存在 GIL 的问题)

所以这个引用计数变量可以通过向多线程共享的所有数据结构添加锁来保持数据安全,这样它们就不会被不一致地修改(译者注:多线程是 share memory 的)。但是,对每个对象或对象组都添加一个锁,这意味着将存在多个锁,这会导致另一个问题,死锁(死锁只有在一个以上的锁时才会发生)。另一个副作用是重复获取和释放锁会降低性能。因此 GIL 是解释器的一个锁,它添加了这样的一个规则,即执行任何 Python 字节码前都需要先获取解释器锁。这样可以防止死锁(因为只有一个锁),并且不会带来太多性能的开销。它可以有效地使任何 CPU 绑定的 Python 程序都是单线程的。

GIL虽然也被 Ruby 等其他语言的解释器使用,但这并不是解决这个问题的唯一方法。一些语言也通过使用引用计数以外的方法(如垃圾收集)来避免 GIL 对线程安全内存管理的要求。但这意味着这些语言通常必须通过添加其他性能提升功能(如JIT编译器)来补偿 GIL 的单线程性能优势的损失。

小结:

Cpython 解释器的内存管理不是线程安全的,Python 使用的是引用计数进行内存管理的,因此存在多线程同时修改对象引用计数的可能,对此需要引入锁机制,但每个对象均加锁会造成死锁等问题,索性只加一个全局锁,这样一来严格意义上讲,不存在多线程了,因此任何一个时间点,只允许一个线程在执行。

1.2、为什么选择了 GIL 这个解决方案

那么,为什么要在 Python 中使用这种看起来如此智障的方案呢?是因为 Python 开发人员的错误决定吗?当然不是,Python 在操作系统还没有线程的概念以前就存在,Python 被设计为易于使用,以便使开发更快,使越来越多的开发人员开始使用它。为了满足 Python 的需要,他们用已存在的 C 库写了许多扩展,但为了避免不一致的修改问题,这些 C 库需要 GIL 提供的线程安全的内存管理机制。另外 GIL 易于实现,可以很容易的添加到 Python 中,而且只需要管理一个锁,因此这提高了单线程程序的性能。这样一来,非线程安全的 C 库也变得更容易集成,这些 C 扩展也成为 Python 被不同社区采用的原因之一。如你所见,GIL 是解决早期 Python 开发人员面临的一个难题的实用解决方案。

1.3、对 Python 多线程的影响

当你看到一个典型的 Python 程序或任何计算机程序时,它们在性能上受 CPU 限制的程序和受 I/O 限制的程序之间是有区别的。受 CPU 密集型程序是那些将 CPU 推向极限的程序。这包括进行数学计算的程序,如矩阵乘法、搜索、图像处理等。I/O 密集型程序是指那些花时间等待来自用户、文件、数据库、网络的输入/输出的程序,等等。由于源可能需要在输入/输出准备就绪之前进行自己的处理,因此,I/O 密集型程序有时必须等待很长一段时间才能从源获得所需的内容,例如,用户正在考虑输入提示或者是自己进程下的某个数据库查询。

下面我们看一个简单执行倒计时的 CPU 密集型程序:

# single_threaded.py
import time
from threading import ThreadCOUNT = 50000000def countdown(n):while n>0:n -= 1start = time.time()
countdown(COUNT)
end = time.time()print('Time taken in seconds -', end - start)# on my system with 4 cores, 3.1 GHz Intel Core i7
# Time taken in seconds - 3.3155179023742676  (2.86-3.3)
# 原文作者给出的时间:Time taken in seconds - 6.20024037361145

现在稍微调整一下代码,用两个线程同时处理:

# multi_threaded.py
import time
from threading import ThreadCOUNT = 50000000def countdown(n):while n>0:n -= 1t1 = 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)# Time taken in seconds - 3.0341949462890625  (2.73-3.03)
# 作者给出的时间是 Time taken in seconds - 6.924342632293701

如你所见,两个版本的完成时间几乎相同。在多线程版本中,GIL 阻止 CPU 密集型多线程并行执行。GIL 对 I/O 密集型的多线程程序性能没有太大的影响,因为在线程等待 I/O 时,锁在线程之间是共享的。但是,如果一个程序的线程完全受到 CPU 的限制,那么它不仅会由于锁的存在而变成单线程,而且执行时间也会增加,如上面的例子所示。这一增长是由锁的获取和释放开销造成的。

1.4、为什么 GIL 还没有被移除

Python 的开发人员对此收到了很多抱怨,但是像 Python 这样流行的语言在不引起向后不兼容问题的情况下,不能带来像删除 GIL 那样重要的变化。显然,GIL 可以被移除,这在过去由开发者和研究者多次完成,但是所有这些尝试都打破了现有的 C 扩展,这很大程度上依赖于 GIL 提供的解决方案。当然,还有很多解决方案,但其中一些解决方案会降低单线程和多线程 I/O 密集型程序的性能,有些解决方案太难了。毕竟,在新版本出现后,您不会希望现有的 Python 程序运行得更慢,对吧?

Guido van Rossum 是 Python 的创建者和 BDFL(Benevolent Dictator For Life,仁慈的独裁者),他在 2007年 9 月的一篇文章 “删除 GIL 并不容易” 中向社区给出了答案:

“I’d welcome a set of patches into Py3k only if the performance for a single-threaded program (and for a multi-threaded but I/O-bound program) does not decrease

“仅当单线程程序(以及 I/O 密集型多线程程序)的性能没有降低时,我才欢迎在 Py3k 中使用一组修补程序”

从那以后的任何尝试都没有满足这个条件。

1.5、为什么不在 Python 3 中移除 GIL

Python 3 确实有机会从零开始进行大量的功能开发,但在这个过程中,打破了一些现有的 C 扩展,这需要更新才能移植到 Python 3。这就是为什么早期版本的 Python 3 被社区采用的速度较慢的原因。

但是为什么 GIL 没有被删除?删除 GIL 会使 Python 3 在单线程性能上比 Python 2 慢,您可以想象这会导致什么结果。因此,结果是 Python 3 仍然拥有 GIL。但是 Python 3 确实给现有的 GIL 带来了很大的改进。

我们前面讨论了 GIL 对“单纯CPU密集型”和“纯 I/O 密集型”多线程的影响,但是对于部分线程是 I/O 密集型的,部分线程是 CPU 密集型的程序呢?在这样的程序中,Python 的 GIL 会使 I/O 密集型的线程长时间等待(strave,挨饿),因此他们没有从 CPU 密集型线程中获取 GIL 的机会。这是一种 Python 内置的机制,该机制强制线程在固定的连续使用间隔之后释放 GIL,但如果没有线程获得 GIL,则同一线程可以继续使用 GIL。

>>> import sys
>>> # The interval is set to 100 instructions:
>>> sys.getcheckinterval()
100

这种机制的问题是,在大多数情况下,CPU 密集型的线程会在其他线程获取 GIL 之前重新获取它本身。这是 David Beazley 的研究和可视化效果。

2009年,Antoine Pitrou 在Python 3.2 中修复了这个问题,他添加了一种机制,查看其他线程丢弃的GIL获取请求的数量,并且不允许当前线程在其他线程有机会运行之前重新获取 GIL。

1.6、怎样处理 Python 的 GIL

如果 GIL 让你不爽,你可以尝试以下几种方法:

Multi-processing vs multi-threading: 可以使用多进程而不是多线程的方式,每个 Python 进程都有自己的 Python 解释器和内存空间,因此不存在 GIL 问题。

多处理与多线程:最流行的方法是使用多处理方法,即使用多个进程而不是线程。每个Python进程都有自己的Python解释器和内存空间,因此GIL不会成为问题。Python有一个多处理模块,让我们可以轻松地创建如下流程:

from multiprocessing import Pool
import timeCOUNT = 50000000
def countdown(n):while n>0:n -= 1if __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)# Time taken in seconds - 1.45 ~ 1.96
# 原文作者给出的时间:Time taken in seconds - 4.060242414474487

与多线程版本相比,性能有了一些提高,但时间并没有减少到我们上面看到的一半,因为进程管理有自己的管理开销。多进程比多线程更繁重,因此需要知道,这可能会成为一个扩展瓶颈。

其他可选的 Python 解释器: Python 有多个解释器实现。CPython、Jython、IronPython 和 PyPy 分别用C、Java、C 和 Python 编写。GIL 只存在于原来的 Python 实现中,即 CPython。如果您的程序及其库可用于其他实现之一,那么您也可以尝试它们。

稍微等等: 尽管许多 Python 用户可以利用 GIL 的单线程性能优势。但多线程编程人员也不必担心,因为 Python 社区中一些最聪明的人正在努力从 CPython 中删除 GIL。其中一种尝试被称为 Gilectomy.。

Python GIL 通常被认为是一个神秘而困难的话题。但请记住,作为一个 Pythonista,只有在编写 C 扩展或者在程序中使用 CPU 密集型的多线程时,才会受到它的影响。如果你想知道 GIL 的底层工作原理,推荐观看 David Beazley 的 Understanding the Python GIL

Python 并发系列 1 —— GIL相关推荐

  1. python并发编程之semaphore(信号量)_Python 并发编程系列之多线程

    Python 并发编程系列之多线程 2 创建线程 2.1 函数的方式创建线程 2.2 类的方式创建线程 3 Thread 类的常用属性和方法 3.1 守护线程: Deamon 3.2 join()方法 ...

  2. Python并发编程系列之多进程(multiprocessing)

    1 引言 本篇博文主要对Python中并发编程中的多进程相关内容展开详细介绍,Python进程主要在multiprocessing模块中,本博文以multiprocessing种Process类为中心 ...

  3. Python并发编程系列之多线程

    1 引言 上一篇博文详细总结了Python进程的用法,这一篇博文来所以说Python中线程的用法.实际上,程序的运行都是以线程为基本单位的,每一个进程中都至少有一个线程(主线程),线程又可以创建子线程 ...

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

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

  5. Python 并发编程之使用多线程和多处理器

    在Python编码中我们经常讨论的一个方面就是如何优化模拟执行的性能.尽管在考虑量化代码时NumPy.SciPy和pandas在这方面已然非常有用,但在构建事件驱动系统时我们无法有效地使用这些工具.有 ...

  6. Python并发与并行的新手指南

    点这里 在批评Python的讨论中,常常说起Python多线程是多么的难用.还有人对 global interpreter lock(也被亲切的称为"GIL")指指点点,说它阻碍了 ...

  7. Python Gevent – 高性能的 Python 并发框架

    From:http://www.xuebuyuan.com/1604603.html Gevent 指南(英文):http://sdiehl.github.io/gevent-tutorial Gev ...

  8. Python并发编程理论篇

    Python并发编程理论篇 前言 很多人学习python,不知道从何学起. 很多人学习python,掌握了基本语法过后,不知道在哪里寻找案例上手. 很多已经做案例的人,却不知道如何去学习更加高深的知识 ...

  9. python 并发编程 多线程 目录

    线程理论 python 并发编程 多线程 开启线程的两种方式 python 并发编程 多线程与多进程的区别 python 并发编程 多线程 Thread对象的其他属性或方法 python 并发编程 多 ...

最新文章

  1. matlab实现瑞利信道需要的步骤,基于Matlab的瑞利信道仿真.docx
  2. 类与接口(二)java的四种内部类详解
  3. Gulp 前端自动化构建
  4. 有生之年必看!千古第一奇书《山海经》到底是怎样的一本书?
  5. Linux命令之 mount -- 文件系统挂载
  6. java showinputdialog_java - JOptionPane.showInputDialog中的多个输入
  7. xlwings 合并单元格 读取_xlwings,让excel飞起来
  8. python bokeh_浅谈python可视化包Bokeh
  9. 反装逼指南:掀起机器学习的神秘面纱
  10. 学习C++项目—— 搭建多线程网络服务框架,性能测试(并发性能测试,业务性能测试,客户端响应时间测试,网络带宽测试)
  11. S2A哨兵数据的波段合成、镶嵌、TOA(大气表观反射率)和裁剪的操作
  12. Android常用的 adb shell命令
  13. cs5460a c语言程序,CS5463程序,有图有程序,大虾来看看,欢迎拍砖!
  14. “我去图书馆”公众号代码抢座的实现
  15. 【高登世德:为资产证券化引入区块链技术】GBCAX
  16. mysql索引,索引结构,索引类型,索引失效
  17. ORAN C平面 Section Type 7
  18. 暴雪战网怎么修改服务器,战网地区如何修改?地区修改流程图文介绍
  19. 如何使用OpenAI API和Python SDK构建自己的聊天机器人
  20. 关于RTSP_RTP_RTCP协议的深刻初步介绍

热门文章

  1. syslog 服务器过程详解
  2. bounded away from zero什么意思?
  3. HP Q2612A加粉(LaserJet 1020等)
  4. 第一次使用JZVideoPlayerStandard播放器
  5. 北京住房建设规划(2006年—2010年)
  6. 汇佳学校家长专栏|家校携手,共创“教育的第三种选择”
  7. 使用 Python 创建您自己的NFT集合(一)自己动手制作中秋月饼上链送给亲朋好友
  8. android studio复写方法,Android Studio 常用快捷键
  9. 【高级React技术】当企业级项目采用Refs 转发和使用 HOC 解决横切关注点问题
  10. python lambda函数加法_python之lambda函数