2019的暑假又准备用Python做个小项目,由于项目涉及高并发特性需要了解一下Python里的多线程与多进程,难免就会了解到CPython里的GIL(Global Interpreter Lock,全局解释器锁),这里想解决一下初次了解GIL可能会提及的一些问题。

什么是GIL?

GIL确保了单个解释器只有一个线程在执行字节码(bytecode),即保证了在字节码层面上Python是线程安全的。对CPython而言单个解释器即对应一个进程,意味着GIL使得Python上的多线程沦为一纸空谈。

有关字节码:参见《python编译过程和执行原理》:编译过程概述

当我们执行Python代码的时候,在Python解释器用四个过程“拆解”我们的代码,最终被CPU执行返回给用户。

首先当用户键入代码交给Python处理的时候会先进行词法分析,例如用户键入关键字或者当输入关键字有误时,都会被词法分析所触发,不正确的代码将不会被执行。

下一步Python会进行语法分析,例如当"for i in test:"中,test后面的冒号如果被写为其他符号,代码依旧不会被执行。

下面进入最关键的过程,在执行Python前,Python会生成.pyc文件,这个文件就是字节码,如果我们不小心修改了字节码,Python下次重新编译该程序时会和其上次生成的字节码文件进行比较,如果不匹配则会将被修改过的字节码文件进行覆盖,以确保每次编译后字节码的准确性。

那么什么是字节码?字节码在Python虚拟机程序里对应的是PyCodeObject对象。.pyc文件是字节码在磁盘上的表现形式。简单来说就是在编译代码的过程中,首先会将代码中的函数、类等对象分类处理,然后生成字节码文件。有了字节码文件,CPU可以直接识别字节码文件进行处理,接着Python就可执行了。

GIL处理多线程的策略有很多,如当前线程阻塞超过一定时间后切换,或遇到长时IO操作时切换等。

为什么有了GIL还需要线程同步?

初看有了GIL,其他用于线程同步的手段(线程锁)就失去了存在的意义,实则不然:GIL确保了字节码层面上的线程安全,但某些数据操作无法用一条字节码实现(类比C中一行代码编译后并不只对应一项汇编操作),以最简单的x = x + 1为例,无论是C还是Python都有以下步骤:

  1. tmp = x + 1
  2. x = tmp

对于这些对应多个字节码的数据操作,仅在字节码层面上的线程安全无法保证数据的可靠性,如线程1执行完字节码1后切换到其他线程,由于线程之间的数据共享,此时数据的一致性很难得到保证。

为什么CPython设计了GIL?

GIL在Python社区中饱受诟病,也有部分不带GIL的Python解释器实现应运而生,那为何CPython当初设计了GIL呢?

官方wiki如此道:

  • 单线程情况下更快。
  • 瓶颈在于 I/O 的多线程环境下更快。
  • CPU 耗时操作发生在 C 库调用上时更快。
  • 编写 C 扩展会更容易:除法你手动指定,否则不会发生 Python 线程切换的问题。
  • 封装 C 库变得更容易,因为不需要考虑线程安全问题。如果该库不是线程安全的,你只需要保证调用时 GIL 是锁定的。

还有一种广为流传的说法:CPython当初在90年代被设计时,多核技术应用很少,GIL的弊端没有如今这么明显。要真是这样,不知能不能说软件设计时开发人员还是得有一定的远见的。

wiki的说法也坐实了Python胶水语言的特性:业务流程用Python,核心模块用C/C++,今后的项目大家也可以尝试充分地利用起Python的胶水性质。使用C++扩展的Python实例

Last modification:August 14th, 2019 at 09:37 am
If you think my article is useful to you, please feel free to appreciate