关于 Python 的 GIL

写这篇文章是因为想彻底搞明白 Python 的 GIL 是什么东西,它为什么而存在,如何避开它。这篇文章完全是按照我个人的思路来完成的,如果有什么错误,请明白的大佬在评论区指正,十分感谢。

GIL 是什么?

GIL 的全程是 Global Interpreter Lock ,即全局解释器锁。它是一个全局的互斥锁,保证了在同一个 Python 解释器进程中,同一时刻只有一个线程在运行。Python 的线程实现中根据不同的操作系统采用了相应的系统原生线程来实现,GIL 使得 Python 的对象和 API 在面对竞争条件时不会出现不可预料的错误。

上面说的都是口水话,没什么用。如果你看不懂的话,建议先去了解一下操作系统这门课程,然后再去看看什么是多线程和多进程的编程以及它们的模型,再来读这篇文章。

为什么有 GIL ?

TL; DR:

  • Python 是一个程序设计语言,它本身没有 GIL 这个东西,Python 官方的解释器 CPython 有 GIL ;
  • CPython 在解释器的实现上是非线程安全的,在权衡了解释器的实现复杂度与解释器的性能之后,他们决定引入 GIL ;
  • 一个大的全局锁比细粒度锁好在执行单线程的程序的时候效率更高,因为要处理的锁的数量低于细粒度锁;
  • 在 I/O 密集型的运算里,使用线程仍然是一个好的选择;
  • 在计算密集型的运算里,可以使用多进程和C语言扩展来规避 GIL 的限制。

详细解释

Python 在执行的时候会被编译成 opcode (operation code) ,一个 opcode 对应的操作系统层面上的操作可能不是原子操作。我们可以想象,对一条 opcode ,Python 需要执行许多条 CPU 指令来完成它。如果这条 opcode 修改了一个对象,那么这个对象就是 临界资源 ,而这段代码则是一个 临界区 。而 GIL 保护了这个临界区,并且使得每一个 opcode 都是原子操作(在其他的解释性语言中,可能用了细粒度锁,例如 Java,这使得 JVM 的实现更加复杂)。可以说,GIL 是一个 Python “操作系统级别”的锁。

那么为什么有了 GIL ,还要在编写多线程程序的时候对程序里的对象加锁呢?因为虽然 GIL 保证了每一个 opcode 都是具有原子性的,但是它没办法保证每一条 Python 语句对应的 opcode 也是原子性的。换句话说,一条 Python 语句可能对应了多条 opcode ,所以需要一个“虚拟机级别”的锁,它使得一个 Python 语句在面对竞争条件的时候不会出现不可意料的错误。

如果给出一个形象的比喻:CPython 是一个虚拟机,Python 代码被编译成 opcode 之后运行在这个虚拟机里,opcode 就是这个虚拟机的“汇编语言”,而虚拟机运行在一个更底层的操作系统上,在执行这个“汇编语言”的时候,要把每一条指令翻译成多条更底层的操作系统的汇编语言指令,所以需要一个叫做 GIL 的东西来保证每一条 opcode 的原子性。

根据《 Python 源码解析》这本书和 Quaro 上的一个回答 ,在 1999 年,两个大牛 Greg Stein 和 Mark Hammond fork 了 Python 1.5 ,做了一个使用细粒度锁的版本,但是经过 Greg Stein 的测试 ,发现在单核上运行的效率甚至低于 GIL 的版本(这很好理解,因为把一把锁拆成了更多的锁,要处理的内容就更多了),于是 CPython 依然使用了 GIL 。实际上,在 2009 年,一位大佬改装了一下 GIL ,使得它的工作方式不要那么粗暴(例如把按照执行的 opcode 数量来切换线程改为按照时间片来切换),让 Python 的多线程效果好了一些。

接下来,许多 Python 的库在编写的时候,就假定了运行它的解释器具有一个 GIL ,于是就依赖了这个特性简化了许多设计,一旦它们被运行在一个没有 GIL 的环境中,很可能会出现很多问题。其实没有 GIL 的 Python 解释器也是存在的,例如 PyPy 和 Jython,但是有一些依赖于 GIL 开发的库就无法使用在这些解释器上。

如何避开 GIL ?

本文核心的部分已经在上一节中写完了。这一节大致说一下如何避开 GIL ,详细的可以进入参考链接多看一看。

使用 C 扩展

如果一定需要用并行来执行计算密集型的任务,Python 就不是一个很好的选择了。但是 Python 是一个非常好的胶水语言,它可以使用自带的 ctype 库来调用 C 语言编写的库。在编写时,使用 Py_BEGIN_ALLOW_THREADS 宏和 Py_END_ALLOW_THREADS 宏即可使解释器在调用该扩展时释放 GIL 。

但是需要注意的是,使用 C 语言编写 Python 扩展,而且要避开 GIL 锁的话,就一定不能调用 Python 的 C API,也决不能使用任何 Python 提供的数据结构,否则会跟 GIL 产生冲突。可以在扩展中使用 pthread 等库来实现多线程,但是 Python 的 API 受到了 GIL 的保护,千万不能混用。

使用多进程

既然 GIL 是保证同一个解释其中只有一个进程运行的,那么多开几个解释器进程不就可以了!Python 提供了这样的库帮助你优雅地实现这样的设计:multiprocessing

这个库中提供了进程池和进程的面向对象封装,你要做的就是调用这些接口然后让你的代码在不同的进程中运行。此外,它还封装了两种 IPC (Inter-Process Communication) 方法 。Queue 是一个消息队列的封装,是并发安全的;Pipe 是一个管道的封装,是非并发安全的,而 multiprocessing 库提供了锁的机制来确保安全。

但是上述的两种 IPC 因为要进行对象的复制,所以可能会拖慢运行速度。为此,multiprocessing 库提供了 Array 和 Value 这两个对 共享内存 的 IPC 方式的封装,使得同一个对象可以同时被多个进程访问,而这也决定了在使用这两个类的时候一定要小心竞争条件,注意加锁。

另外,还有一个 Manager 类,虽然比使用上述的两个共享内存的封装类更加慢一些,但是它很强,强到在不同的机器上使用网络通信的进程都可以用它来简单地同步。

使用没有 GIL 的解释器

这里就不展开说了,例如 PyPy 和 Jython。但是如上面所说的那样,因为没有 GIL ,所以有很多依赖于 GIL 的库无法在这两个解释器中使用。

参考

  1. 为什么 Python 被设计为有 GIL ?
  2. Python有GIL为什么还需要线程同步? – nnop的回答 – 知乎
  3. Python有GIL为什么还需要线程同步? – 林诚的回答 – 知乎
  4. 如何避开 GIL ?
  5. Python的GIL是什么鬼,多线程性能究竟如何
  6. python中的多线程和GIL锁
  7. 互斥锁,同步锁,临界区,互斥量,信号量,自旋锁之间联系是什么? – Tim Chen的回答 – 知乎

CC BY-NC-SA 4.0 关于 Python 的 GIL by James & Alice is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

关于 Python 的 GIL》上有1条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据