CPython中的全局解释器锁(GIL)是什么?

277

什么是全局解释器锁,为什么它是一个问题?

有很多关于从Python中删除GIL的讨论,我想了解为什么这样做非常重要。我自己从未编写过编译器或解释器,因此请提供详细信息,以便我能够理解。


4
观看David Beazley向您介绍有关GIL的所有信息,链接为https://www.youtube.com/watch?v=Obt-vMVdM8s。 - hughdbrown
1
这是我之前写的一篇关于Python中GIL和线程的较长文章。它详细介绍了相关内容:http://jessenoller.com/2009/02/01/python-threads-and-the-global-interpreter-lock/ - jnoller
这里有一些演示GIL效果的代码:https://github.com/cankav/python_gil_demonstration - Can Kavaklıoğlu
3
我认为这是对GIL最好的解释。请阅读。http://www.dabeaz.com/python/UnderstandingGIL.pdf - suhao399
我发现这篇文章很有用:https://realpython.com/python-gil/ - qwr
Python - GIL 的过去、现在和未来,值得一读,并提供了进一步研究的链接以获取详细信息。 - Thingamabobs
8个回答

242

Python的全局解释器锁(GIL)旨在序列化不同线程对解释器内部的访问。在多核系统中,这意味着多个线程无法有效地利用多个核心。(如果GIL没有导致这个问题,大多数人也不会关心GIL——只是由于多核系统越来越普遍,才将其提升为一个问题)。如果你想详细了解它,可以观看这个视频或查看这组幻灯片。可能有点太多信息了,但你确实要求了细节 :-)

请注意,Python的GIL只是针对参考实现CPython的一个问题。Jython和IronPython没有GIL。作为Python开发人员,你通常不会遇到GIL,除非你正在编写C扩展。C扩展编写者需要在其扩展进行阻塞I/O时释放GIL,以便Python进程中的其他线程有机会运行。


53
好的回答 - 基本上意味着 Python 中的线程只适用于阻塞 I/O;您的应用程序永远不会超过 1 个 CPU 核心的处理器使用率。 - Ana Betts
12
作为Python开发者,除非你编写C扩展程序,否则通常不会遇到GIL问题。你可能不知道多线程代码运行缓慢的原因是GIL,但你肯定会感受到它的影响。令人惊讶的是,在Python中利用32核服务器需要使用32个进程,带来了大量的附加开销。 - Basic
7
@PaulBetts说的不是真的。很可能性能关键的代码已经使用了可以释放GIL的C扩展,例如regexlxmlnumpy模块。Cython允许在自定义代码中释放GIL,例如b2a_bin(data) - jfs
6
你可以使用multiprocessing模块来使处理器利用率达到1个以上的CPU。创建多个进程比创建多个线程更加耗费资源,但如果你真的需要在Python中实现并行处理,这是一种选择。 - AJNeufeld
1
@david_adler 是的,现在还是这种情况,而且可能会持续一段时间。但这并没有真正阻止Python在许多不同的工作负载中非常有用。 - Vinay Sajip
显示剩余3条评论

64
假设你有多个线程,它们并不真正互相触及到对方的数据。这些线程应该尽可能独立地执行。如果你需要获取一个“全局锁”才能(比如)调用一个函数,那么这个锁就可能成为瓶颈。在这种情况下,你可能无法从多线程中获得任何好处。
把它用现实世界的比喻来说:想象一下,在一家只有一个咖啡杯的公司里,有100个开发人员工作。大多数开发人员将花费他们的时间等待咖啡而不是编码。
这些都与Python无关 - 我不知道Python最初需要GIL的详细信息。然而,希望这给你提供了更好的概念。

除了等待咖啡杯似乎是一个相当 I/O 绑定的过程,因为他们肯定可以在等待杯子的时候做其他事情。GIL 对于大部分时间都在等待的 I/O 重型线程几乎没有影响。 - Cruncher

47

首先让我们理解Python GIL提供了什么:

所有操作和指令都是在解释器中执行的。GIL确保解释器在特定瞬间只由一个线程持有。而您的带有多个线程的Python程序在单个解释器中工作。在任何特定瞬间,这个解释器只被一个线程持有。这意味着只有持有解释器的线程在任何时刻运行。

那么这为什么是个问题:

您的计算机可能有多个内核/处理器。多个内核允许多个线程同时执行,即多个线程可以在任何特定时间执行。但由于解释器只被单个线程持有,即使其他线程可以访问内核,它们也不做任何事情。因此,您没有获得由多个内核提供的任何优势,因为在任何瞬间,只有一个内核正在使用,即当前持有解释器的线程正在使用的内核。所以,您的程序将花费与单线程程序相同的时间来执行。

然而,潜在的阻塞或长时间运行的操作,比如I/O、图像处理和NumPy数字处理,发生在GIL之外。摘自这里。因此,对于这样的操作,尽管存在GIL,多线程操作仍然比单线程操作快。所以,GIL并不总是瓶颈。

编辑: GIL是CPython的实现细节。IronPython和Jython没有GIL,因此在它们中可以实现真正的多线程程序,但我从未使用过PyPy和Jython,也不确定是否有效。


4
注意:PyPy 拥有 GIL。参考资料:http://doc.pypy.org/en/latest/faq.html#does-pypy-have-a-gil-why。而 IronPython 和 Jython 不拥有 GIL。 - Tasdik Rahman
确实,PyPy有GIL,但IronPython没有。 - Emmanuel
@Emmanuel编辑了答案,删除了PyPy并包括了IronPython。 - Akshar Raaj

29

Python 3.7文档

我还想强调一下Python threading文档中的以下引用:

CPython实现细节:由于全局解释器锁(Global Interpreter Lock),在CPython中,仅有一个线程可以执行Python代码(尽管某些面向性能的库可能会克服此限制)。如果您希望应用程序更好地利用多核计算机的计算资源,则建议使用multiprocessingconcurrent.futures.ProcessPoolExecutor。但是,如果您想同时运行多个I/O绑定任务,则线程仍然是适当的模型。

这链接到“全局解释器锁”词汇表条目,其中解释了GIL意味着Python中的线程并行不适用于CPU绑定任务

CPython解释器使用的机制,以确保只有一个线程在任何时候执行Python字节码。这使得CPython实现更加简单,通过隐式地使对象模型(包括关键的内置类型,如dict)安全免受并发访问的影响。锁定整个解释器使得解释器更容易成为多线程的,但牺牲了多处理器机器提供的大部分并行性。
然而,一些扩展模块,无论是标准的还是第三方的,都是设计为在进行计算密集型任务(如压缩或哈希)时释放GIL。此外,在进行I/O时总是释放GIL。
过去创建“自由线程”解释器(在更细的粒度上锁定共享数据的解释器)的努力并不成功,因为在常见的单处理器情况下性能会受到影响。人们认为,克服这个性能问题会使实现变得更加复杂,因此维护成本更高。
此引用还意味着dicts和因此变量赋值也是线程安全的作为CPython实现的细节。

接下来,multiprocessing包的文档解释了它如何通过生成进程并暴露类似于threading的接口来克服GIL:

multiprocessing是一个支持使用类似于threading模块的API生成进程的包。该包提供本地和远程并发,通过使用子进程而不是线程来有效地绕过全局解释器锁定。因此,multiprocessing模块允许程序员充分利用给定机器上的多个处理器。它可以在Unix和Windows上运行。

关于 concurrent.futures.ProcessPoolExecutor 的文档则解释了它使用multiprocessing作为后端:

ProcessPoolExecutor类是Executor子类,它使用进程池异步执行调用。 ProcessPoolExecutor使用multiprocessing模块,这使其可以回避全局解释器锁定,但这也意味着只能执行和返回可挑选的对象。

这与另一个基类ThreadPoolExecutor形成对比,后者使用线程而不是进程

ThreadPoolExecutor是Executor子类,它使用线程池异步执行调用。

由此我们得出结论,ThreadPoolExecutor仅适用于I/O绑定任务,而ProcessPoolExecutor也可以处理CPU绑定任务。

进程 vs 线程实验

Python中的多进程与多线程方面,我进行了一项实验性分析。

结果快速预览:

enter image description here

在其他语言中

这个概念似乎不仅存在于Python中,同样适用于Ruby等编程语言:https://en.wikipedia.org/wiki/Global_interpreter_lock

它提到了以下优点:

  • 提高了单线程程序的速度(不需要单独获取或释放所有数据结构上的锁),
  • 容易集成不支持多线程的C库,
  • 易于实现(一个单一的GIL比使用无锁解释器或使用细粒度锁要简单得多)。

但是JVM似乎没有GIL也能正常运行,所以我想知道是否值得这样做。下面的问题问GIL首先存在的原因:Why the Global Interpreter Lock?


21

Python在真正意义上不允许多线程。它有一个多线程包,但如果你想要使用多线程来加速代码运行,通常不建议使用它。Python有一个叫做全局解释器锁(GIL)的结构。

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

GIL确保只能有一个“线程”在任何时候执行。一个线程获取GIL,做一些工作,然后将GIL传递给下一个线程。这个过程非常快速,所以对于人眼来说,看起来好像你的线程在并行执行,但实际上它们只是轮流使用同一个CPU核心。所有这些GIL传递会增加执行的开销。这意味着,如果你想让你的代码运行得更快,那么经常使用线程包可能不是一个好主意。

使用Python的线程包也有一些优点。如果你想同时运行某些东西,并且效率不是一个问题,那么完全可以使用它。或者如果你正在运行需要等待某些东西(如一些IO)的代码,那么使用它可能是很有意义的。但是线程库不能让你使用额外的CPU核心。

多线程可以通过操作系统(通过多进程),一些调用你的Python代码的外部应用程序(例如Spark或Hadoop),或者你的Python代码调用的一些代码(例如:你可以让你的Python代码调用一个执行昂贵的多线程操作的C函数)来进行外包。


16
每当两个线程都可以访问同一个变量时,就会出现问题。 例如,在C++中,避免这个问题的方式是定义一些互斥锁,以防止两个线程在同一时间进入对象的setter。
Python中支持多线程,但在粒度上,不能同时执行两个线程执行Python指令。 正在运行的线程正在获取称为GIL的全局锁。
这意味着,如果您开始编写一些多线程代码以利用您的多核处理器,则性能不会提高。 通常的解决方法是使用多进程。
请注意,如果您在自己编写的C方法中进行操作,可以释放GIL。
使用GIL并非Python的固有问题,而是某些解释器(包括最常见的CPython)的问题。 (#编辑,请参见评论)
在Python 3000中仍然存在GIL问题。

Stackless仍然有GIL。 Stackless不会改善线程(如模块)-它提供了一种不同的编程方法(协程),试图绕过这个问题,但需要非阻塞函数。 - jnoller
3.2 版本中的新 GIL 怎么样? - new123456
只是要补充一点,如果只有一个线程会更新内存,那么你就没有问题/需要互斥锁/信号量。@new123456 它减少了争用并更好地调度线程,而不会损害单线程性能(这本身就很令人印象深刻),但它仍然是全局锁。 - Basic

1

我想分享一本名为《Visual Effects多线程编程》的书中的一个例子。这是一个经典的死锁情况。

static void MyCallback(const Context &context){
Auto<Lock> lock(GetMyMutexFromContext(context));
...
EvalMyPythonString(str); //A function that takes the GIL
...    
}

现在考虑导致死锁的事件序列。
╔═══╦════════════════════════════════════════╦══════════════════════════════════════╗
║   ║ Main Thread                            ║ Other Thread                         ║
╠═══╬════════════════════════════════════════╬══════════════════════════════════════╣
║ 1 ║ Python Command acquires GIL            ║ Work started                         ║
║ 2 ║ Computation requested                  ║ MyCallback runs and acquires MyMutex ║
║ 3 ║                                        ║ MyCallback now waits for GIL         ║
║ 4 ║ MyCallback runs and waits for MyMutex  ║ waiting for GIL                      ║
╚═══╩════════════════════════════════════════╩══════════════════════════════════════╝

1

为什么Python(包括CPython和其他版本)使用GIL

来源于http://wiki.python.org/moin/GlobalInterpreterLock

在CPython中,全局解释器锁(GIL)是一个互斥锁,它防止多个本地线程同时执行Python字节码。这个锁主要是必需的,因为CPython的内存管理不是线程安全的。

如何从Python中删除它?

像Lua一样,也许Python可以启动多个VM,但Python没有这样做,我猜应该有其他原因。

在Numpy或其他一些Python扩展库中,有时将GIL释放给其他线程可以提高整个程序的效率。


Python可以使用标准库中的multiprocessing包启动多个虚拟机。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接