总结 | 尹立博:Python 全局解释器锁与并发 | AI 研习社第 59 期猿桌会

AI 科技评论按:作为排名靠前的最受欢迎和增长最快的编程语言之一,Python 是一种多用途、高级别、面向对象、交互式、解释型和对用户非常友好的编程语言,拥有卓越的可读性和极高的自由度。而为了能利用多核多线程的的优势,同时又要保证线程之间数据完整性和状态同步,Python 官方的、最广泛使用的解释器——CPython 往往会采取最简单的加锁的方式——全局解释器锁(GIL)。

然而,GIL 的设计有时会显得笨拙低效,并对语言的并发性带来严重限制,但是此时由于内置库和第三方库已经对 GIL 形成了巨大的依赖,想改变 GIL 反而变得困难了。不过实际上,Python 生态系统中存在诸多工具可以解决这一问题。

近日,在 AI 研习社公开课上,毕业于澳大利亚国立大学的尹立博介绍了全局解释器锁(GIL)和提升并发性的不同思路。公开课回放视频请点击文末阅读原文。

尹立博:毕业于西澳大利亚大学和澳大利亚国立大学。现在堪培拉 Seeing Machines 公司担任数据分析师,日常使用 Python 数据工具对大量时序数据进行管理、分析与可视化开发。

分享主题:Python 全局解释器锁与并发

分享提纲

1、全局解释器锁 (GIL)

2、多进程 (multiprocessing)

3、多线程 (multithreading)

4、异步 (async)

5、分布式计算(以 Dask 为例)

AI 研习社将其分享内容整理如下:

今天要跟大家分享的是 Python 全局解释器锁与并发。我会先介绍一下全局解释器锁 (GIL))的概念和影响;接下来会借助几个案例分析来展示 Python 通过多进程、多线程和异步、分布式计算来达成并发的几种方式;最后会介绍一套分布式计算工具——Dask。

全局解释器锁 (GIL)

GIL 的概念用简单的一句话来解释,就是「任一时刻,无论线程多少,单一 CPython 解释器只能执行一条字节码」。这个定义需要注意的点包括:

第一,GIL 不属于 Python 语言定义,而是 CPython 解释器实现的一部分; 第二,其他 Python 解释器不一定有 GIL。例如 Jython (JVM) 和 IronPython (CLR) 没有 GIL,而 PyPy 有 GIL; 第三,GIL 并不是 Python 的专利。其他语言也有 GIL,尤其是动态语言,如 Ruby MRI。

说到 GIL,就不得不提 Python 线程模型,它的运行方式如下:

  • CPython 使用 OS 原生线程,由 OS 负责调度;
  • 每个解释器进程有唯一的主线程和用户定义的任意数量子线程;
  • GIL 是字节码层面上的互斥锁。刚刚定义中提到的 PyThread_type_lock 就是 OS 互斥锁的别名
  • 每个解释器进程有且仅有一把锁;
  • 当解释器启动时,主线程即获取 GIL;
  • 一个线程持有 GIL 并执行字节码时,其他线程处于阻塞状态。

GIL 被加到 CPython 解释器中,是有其原因的。在 1992 年,单 CPU 是合理的假设!多核则是 2005-2006 年前后才普及,此外,GIL 的优势还包括:

  • 简化解释器实现;
  • 优化单进程性能;
  • 简化 C 扩展库的整合。

Python 有两种多任务模型:一种叫做协作式 (cooperative) 多任务;另一种叫抢占式 (preemptive) 多任务。

协作式多任务:

  • 在 I/O 前主动释放 GIL,I/O 之后重新获取。这可以在 C 源代码中使用 Py_BEGIN_ALLOW_THREADS / Py_END_ALLOW_THREADS 宏实现
  • 这种多任务方式能够提升代码性能!

抢占式多任务:

  • 间歇性挂起活跃进程,交由 OS 重新调度
  • Python 2:每执行 100 个字节码,当前进程就会被挂起
  • Python 3.2+: 每隔 5 毫秒
  • 这种多任务方式不提高代码性能,但使得多个任务能在同一时间段内执行

接下来可以进展到去除 GIL。这是很多 Python 用户十分期待的事情,但是短期内是不太可能实现的,它的难点包括:

第一,技术问题

  • Guido 要求不降低单线程执行效率
  • 兼容现有引用计数与垃圾回收机制
  • 兼容现有 C 扩展

第二,在社区友好性上,不显著提高开发难度。

尽管如此,我们也可以看到一些现有去除 GIL 的实验性的方案:

  • Gilectomy:尝试将 GIL 换成若干小锁,然而这种方案严重降低了 Python 的性能。首先,它会使得多线程竞争同一把锁。其次,它在将 GIL 换成若干小锁后,将严重降低缓存的命中率。
  • PyPy:实验性分支支持软件事务内存 (STM),不过 STM 目前还是一个相对少见的机制,可解决当前很多问题,但是实现非常困难——尤其在像 Python 这种高度动态的语言当中。
  • Starlark:这种方案并非去掉 GIL,而是一门兼容部分 Python 语法,并发执行字节码的新语言。它目前用于 Google Bazel 编译系统,我个人认为这是一个非常有意思的未来趋势。

既然现在去除 GIL 的方案都有很多弊端,并且短期内我们也无法让 GIL 从 Python 中被去除,我们最常见的解决方案就是避开 GIL,主要通过两种手段实现:

第一种是多解释器进程并发 (multiprocessing) 第二种是避免执行 Python 字节码,常见的方法有:Cython ctypes、部分 NumPy 函数释放 GIL、Numba JIT「nogil=True」,以及 TensorFlow/PyTorch JIT。

多进程(multiprocessing)和多线程(multithreading)

进入案例分析前,先介绍几个相关的概念。

首先介绍一下并行与并发的区别:

  • 并发(concurrency):是指多个操作可以在重叠的时间段内进行,例如在第一个时间片内,线程 A 执行,线程 B 阻塞;第二个时间片内,线程 B 等待 I/O,而线程 A 执行;第三个时间片内,线程 A 执行,而线程 B 还在等待 I/O。
  • 并行(parallelism):是指多个操作在同一时间点上进行。无论在哪个时间片里,两个线程可能同时处于某一状态。例如在第一个时间片内,线程 A 执行,线程 B 执行;第二个时间片内,线程 A 等待 I/O,线程 B 也在等待 I/O ;第三个时间片内,线程 A 执行,而线程 B 也 执行。

多线程意味着我们在使用并发这种线程模型,而多进程则是在使用并行这一线程模型,其各有利弊:

  • 多线程并发的优势为:可共享内存空间,方便交换数据;劣势为:会同时写入内存将导致数据损坏。
  • 多进程并行的优势为:内存空间独立(恰来自其劣势);劣势为:进程间交互需要序列化-通信-反序列化。

接下来我们将通过一个案例来尝试 Python 并发的几种不同解决方案的案例:

(关于尝试 Python 并发的几种不同解决方案的案例讲解,请回看视频 00:19:05 处)

这就讲到多进程(multiprocessing)这一概念,它的适用场景包括:

  • CPU 占用率高
  • 子进程间通信简单
  • 相关变量和函数可被序列化,但占用内存较小

如果想知道更多内容,大家可参见文档:

https://docs.python.org/3/library/multiprocessing.html https://docs.python.org/3/library/concurrent.futures.html

接下来进入到多进程解决方案的案例讲解:

(关于多进程解决方案的案例讲解,请回看视频 00:23:25 处)

之后要讲到多线程 (multithreading),多线程的使用场景包括:

  • CPU 占用率低
  • I/O 负载高
  • 子任务需要共享内存

如要了解更多内容,可以参见文档:

https://docs.python.org/3/library/threading.html https://docs.python.org/3/library/queue.html

(关于多线程解决方案的案例讲解,请回看视频 00:33:25 处)

再看一下 Python 多线程编程难点,下面这些难点有些针对 Python,有些是所有多线程共通的难题:

第一,CPython 的线程切换可能在任意字节码之间发生,而 Python 指令不具有原子性 第二,每次访问受限资源都需获取锁 第三,锁不具有强制性,即使忘记获取锁,代码也可能运行 第四,竞争状态难以复制

我们看一个相关的案例——多线程计数器:

(关于多线程计数器的案例讲解,请回看视频 00:37:00 处,http://www.mooc.ai/open/course/569?=aitechtalkyinlibo)

异步 (async)

接着讲一下异步 (async)。Python 中的异步是一种在单一线程内使用生成器实现的协程,比线程能更高效地组织非阻塞式任务。协程的切换由 Python 解释器内完成。当然,其他语言也有异步编程,比如 Go 语言的 goroutine,以及 Nginx 用 C 实现了异步编程。

关于更多异步编程的内容,大家可参见文档:

https://docs.python.org/3/library/asyncio-task.html

看案例之前,先比较一下异步与线程。与线程相比,异步的优劣势分别为:

优势:

  • 简单的多任务模型
  • 明确的协程切换点
  • 系统开销远小于 OS 原生线程

劣势:

  • 有相对独立的生态系统
  • 与其他并发模型混用较难
  • API 仍未稳定

下面我们看异步的案例:

(关于异步的案例讲解,请回看视频 00:46:05 处)

分布式计算(以 Dask 为例)

最后讲一下分布式计算,本堂课中的分布式计算以 Dask 为例。

Dask 是一种基于运算图的动态任务调度器,可使用动态调度器扩展 NumPy 和 Pandas。左边这个图就是 Dask 的运算图。

(关于 Dask 运算图的讲解,请回看视频 00:55:45 处)

与另一种分布式计算方法 Spark 比较,Dask 的特性非常鲜明:

  • 它是一个纯 Python 实现
  • 无需遵循 map-reduce 范式
  • 细粒调度带来较低的延迟

在 Dask 中,我们更关注的是 Distributed。它是 Dask 在异构集群上的扩展。它的网络结构遵循客户 – 调度器 – 工作节点这样的形式,因此要求所有节点拥有相同的 Python 运行环境。

接下来我们看一个简单的案例:

(关于该案例讲解,请回看视频 00:59:45 处)

最后放上今天这堂课涉及到的内容的演讲,基本都能在 youtube 上进行观看。

Dave Beazley: Understanding the Python GIL, PyCon 2010

https://www.youtube.com/watch?v=Obt-vMVdM8s https://www.dabeaz.com/python/UnderstandingGIL.pdf

Dave Beazley: Embracing the Global Interpreter Lock (GIL), PyCodeConf 2011

https://www.youtube.com/watch?v=fwzPF2JLoeU

Larry Hastings: Python's Infamous GIL, PyCon 2015

https://www.youtube.com/watch?v=KVKufdTphKs

Larry Hastings: Removing Python's GIL: The Gilectomy, PyCon 2016 https://www.youtube.com/watch?v=P3AyI_u66Bw

A Jesse Jiryu Davis: Grok the GIL Write Fast And Thread Safe Python, PyCon 2017

https://www.youtube.com/watch?v=7SSYhuk5hmc

Raymond Hettinger: Keynote on Concurrency, PyBay 2017

https://www.youtube.com/watch?v=9zinZmE3Ogk https://pybay.com/site_media/slides/raymond2017-keynote/index.html

Dave Beazley: Fear and Awaiting in Async: A Savage Journey to the Heart of the Coroutine Dream

https://www.youtube.com/watch?v=E-1Y4kSsAFc

Robert Smallshire: Coroutine Concurrency in Python 3 with asyncio

https://www.youtube.com/watch?v=c5wodlqGK-M •Matthew Rocklin: Dask for ad hoc distributed computing https://www.youtube.com/watch?v=EEfI-11itn0

Matthew Rocklin: Dask: A Pythonic Distributed Data Science Framework, PyCon 2017

https://www.youtube.com/watch?v=RA_2qdipVng

原文发布于微信公众号 - AI科技评论(aitechtalk)

原文发表时间:2018-12-02

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券