一起解锁 GIL 的新姿势
学 Python 的人有一个东西始终规避不开,那就是 GIL (Global Interpreter Lock)。顾名思义,它使得任何时刻仅有一个线程在执行。即便在多核心处理器上,使用 GIL 的解释器也只允许同一时间执行一个线程。
本场 Chat 将带着大家走进 Python 新世界,一起解锁 GIL 的新姿势。
通过本场您将学到:
- 关于 GIL 的缺陷,以及在 Python 3 中对于 GIL 的改进。
- 我们常见规避 GIL 的手段。
- 在处理各类 I/O 场景的时候,我们如何提升性能?
面向对象:所有 Pythoneer。
学 Python 的人有一个东西始终规避不开,那就是 GIL(Global Interpreter Lock)。顾名思义,它使得任何时刻仅有一个线程在执行。即便在多核心处理器上,使用 GIL 的解释器也只允许同一时间执行一个线程。
Python 中 GIL 存在的意义在于,利用更小的成本,保证了基本操作的安全性。曾经 Python 社区尝试在 Python 中移除 GIL,换用更细粒度的锁来管理 Python 的线程,显而易见,更细粒度的管理,意味着更大的开销以及实现成本。最后得到的版本其性能表现并不如标准版本的 CPython。
所以说到现在,我们有没有去系统的了解过 Python 中的线程和 GIL 呢?
首先 Python 中的线程,是对于系统线程(POSIX 系统下的 pthread 或者 Windows Thread)的一系列封装,Python 在基础上额外的封装了一些数据,我们先来看看 Python 线程的核心 PyThreadState:
如前面所说,Python 对于线程做了一系列的封装,以方便我们记录额外的数据信息。
现在有个新的问题,在 GIL 的控制下,线程之间的切换到底是怎样的。
实际上分为两种情况,I/O 密集 / CPU 密集,我们先来看个图:
图(2)
图(3)
图2 是在 I/O 操作中的线程切换,我们能看到,当 Python 进行 I/O 操作的时候(比如 read、write、recv 等等)会检查是否有必要进行 GIL 的切换,如果有,则切换。
图3 是在 CPU 密集型线程中线程的状态,在早期版本中,当当前线程执行了一定数量(通过 sys.setcheckinterval()
设置)的 opcode 时,会进行检查,切换 GIL 。
现在大家可能想知道 GIL 是怎么进行切换的。其实 GIL 切换很简单,我们可以称之 GIL 四步走
- 重置 opcode 计数器
- 处理切换信号
- 释放 GIL
- 重新获取 GIL
是不是很简单?实际上就这么简单,我们来看一下真正的实现吧。
讲了半天,现在有个问题,GIL 到底是什么?他到底有什么缺陷?
是这样的,CPython 为了保证线程状态同步,实现了一个巨大的锁,这个锁的作用域涵盖整个 CPython 的生命周期。这个锁是由操作系统提供的互斥锁以及其余的一些条件变量组成的,并不是我们常规意义上的互斥锁。而 GIL 则是这个锁的实例。
好了,我们画一下重点,GIL 到底是什么?
- 一个互斥锁
- 用来进行进程状态同步
- 包含其余的一些变量
- 彼此之间通过信号通信
为了加深大家理解,我们来用伪代码来描述一下这个锁首先,锁的实现很简单,三个变量,分别表示当前状态,互斥锁,以及信号:
然后,我们加锁解锁的伪代码其实也很简单:
好了,我们现在要来讨论下 GIL 到底有什么缺陷?
在讨论缺陷之前,我们首先达成一个共识,在操作系统中,线程的切换是由操作系统进行调度的。在达成这个共识后,我们便可以知道,Python 使用 GIL 来控制任何时候只能有一个线程运行,在这个过程中 Python 来控制线程何时释放 GIL,而底层的 OS 来控制哪个线程获取 GIL。Python 在遇到两种情况下强制某个线程释放 GIL,一是该线程要进行 IO 操作,二是该线程已经执行了某个数量的 opcode。这样的机制就带来了两个问题:
- 使用固定数量的 opcode 做为单位来强制切换线程,必然是粗糙的,不准确的,因为有的代码可能执行的很快,而有的则更耗时。让OS来选择哪个线程执行,有时候是无法预测的(尤其在多CPU下)。比如有可能造成某个线程刚刚释放GIL,却马上又获取了GIL。这一点,我们马上来用两个图讨论下。
前面我们提到,「OS 来选择哪个线程执行,有时候是无法预测的(尤其在多 CPU 下)。比如有可能造成某个线程刚刚释放 GIL,却马上又获取了 GIL」,我们来看两个例子。
第一个例子很简单:
这个我们能很清楚的看到,线程 1 在释放 GIL 之后发送了信号,操作系统这个时候切换到线程 2 进行执行,线程 2 成功获取到锁,继续向下执行,线程 1 则继续等待锁的释放。
看起来很美好是不是?那么接下来有个例子就不这么美好了。
在这个例子中,我们能很清晰的看到,线程 1 在释放锁之后,立刻获取到锁,继续向下执行,而线程 2 则一直处于等待状态。那么这样可能存在的极端情况就是,线程 2 需要等到线程1执行完后才能开始执行。
针对这些问题,Antoine Pitrou 对 GIL 的实现进行了重写(这也是 1992 年之后 Python 对于 GIL 的首次修改,在 Python 3.2 中被正式引进),他曾经在 Python-Dev 邮件列表中和大家讨论过相关实现(参见:[Python-Dev] Reworking the GIL)。
使用固定的时间(默认是 5ms)而不是固定数量的 opcode 来进行线程的强制切换,这样就解决了上述问题 1,即「使用固定数量的 opcode 做为单位来强制切换线程,必然是粗糙的,不准确的,因为有的代码可能执行的很快,而有的则更耗时。」
但是这样还是没法彻底解决这个问题,因为这个固定时间并没法准确的描述内存中线程的切换情况,假设有个高优先级的线程已经执行完 I/O 操作需要继续往下执行,但是因为没有到设定的时间,依旧只能继续等待。
在线程释放 GIL 后,开始等待,直到某个其它线程获取 GIL 后,再开始去尝试获取 GIL。这样可以避免此前获得 GIL 的线程,不会立即再次获取 GIL,但仍然无法保证优先级高的线程获取 GIL。
在 Python 3.2 引入全新的 GIL 后,其性能表现相对于以往提升很多,但是其性能表现依旧不如人意。
所以在目前而言,对于 CPU 密集型的应用,我们主流的规避 GIL 的方案有两种,一种是写 C Extension,一种是用多进程。在针对 I/O 密集的场景,为了避免线程的不可控,我们也会考虑第三方的协程方案。在 Python 3.4 以后我们会考虑官方的 asyncio 方案。
在现阶段,使用 CPython 提供的 API 编写 C Extension 看起来是一个不错的主意,不过这样也有着一些问题目前 Python 的 C API 设计并不合理,以至于现在社区推荐的写 C 扩展的方式是使用 CythonC API 在需要访问 CPython 内的数据时,依旧需要受制于 GIL 以保证线程安全。
C Extension 存在兼容性问题,当切换其余的 Python 实现的时候,比如 PyPy 等,C Extension 的兼容性无法得到保证。
目前另外一种比较主流的规避 GIL 的方式就是使用多进程 ,我们最常见的就是 multiprocessing。
multiprocessing 由来已久,最早可以追溯至 PEP 371 ,当时提出在 2.6/3.0 将一个叫做 pyProcessing 的库整合进官方,提供更好的封装,让用户使用进程的时候,不必像原始人一样直接调用系统 API,并在封装的时候做了跨系统兼容。
multiprocessing 目前而言,已经成为我们日常使用的必备品之一,但是其也有很多缺陷,很简单的,我们对于进程的掌控不够,也没法很方便的获取进程的执行结果以及相关异常。CPython 官方也意识到了这个问题,对于对于很多需要实现异步任务的场景来说,目前的开发模式略有一点不友好的,开发者往往需要做一些额外的工作,才能比较干净的实现一些异步的需求。
为了解决这样的窘境,09 年 10 月,Brian Quinlan 先生提出了 PEP 3148,在这个提案中,他提出将我们常用的 multiprocessing 和 threding 模块进行进一步封装,达成较好的支持异步操作的目的。最终这个提案在 Python 3.2 中被引入。也就是今天的 concurrent.future。
第八届中国 Python 开发者大会 PyCon China 2018 ,由 PyChina.org 发起,由来自 CPyUG / TopGeek 等社区的 30 位组织者,近 150 位志愿者在北京、上海、深圳、杭州、成都等城市举办。致力于推动各类 Python 相关的技术在互联网、企业应用等领域的研发和应用。
今年的 PyCon China 2018 将主会场设在北京。北京是 Pythoneer 最集中的城市。我们将以上午千人峰会的主会场和下午 4 个分会场的形势来最大程度地去满足各领域 Pythoneer 们的诉求。(北京核心组织者李者璈的建议下首次开设 Python 女性专场)。
好啦,有兴趣吗!来现场和嘉宾面对面沟通交流吧!
本文首发于GitChat,未经授权不得转载,转载需与GitChat联系。
阅读全文: http://gitbook.cn/gitchat/activity/5b9f3cdb89a0a3128f855401
一场场看太麻烦?订阅GitChat体验卡,畅享300场chat文章!更有****下载、****学院等超划算会员权益!点击查看