261

什么是全局解释器锁,为什么会出现问题?

从 Python 中删除 GIL 引起了很多争论,我想了解为什么这如此重要。我自己从来没有写过编译器或解释器,所以不要吝啬细节,我可能需要他们理解。

4

8 回答 8

233

Python 的 GIL 旨在序列化从不同线程对解释器内部的访问。在多核系统上,这意味着多线程不能有效地利用多核。(如果 GIL 没有导致这个问题,大多数人不会关心 GIL - 它只是因为多核系统的日益普及而被提出来。)如果您想详细了解它,您可以观看此视频或查看这组幻灯片。可能信息太多,但是您确实要求提供详细信息:-)

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

于 2009-08-18T14:53:17.807 回答
63

假设您有多个线程并没有真正接触到彼此的数据。这些应该尽可能独立地执行。如果你有一个“全局锁”,你需要获取它才能(比如说)调用一个函数,那最终可能会成为一个瓶颈。首先,您可能不会从拥有多个线程中获得太多好处。

把它放到一个现实世界的类比中:想象一下 100 名开发人员在一家只有一个咖啡杯的公司工作。大多数开发人员会花时间等待咖啡而不是编码。

这些都不是 Python 特有的——我不知道 Python 首先需要 GIL 的细节。但是,希望它能让您更好地了解一般概念。

于 2009-08-18T14:53:04.830 回答
44

我们先来了解一下python GIL提供了什么:

任何操作/指令都在解释器中执行。GIL 确保解释器在特定时刻由单个线程持有。并且您的具有多个线程的 python 程序在单个解释器中工作。在任何特定时刻,这个解释器都由一个线程持有。这意味着只有持有解释器的线程在任何时刻都在运行

现在为什么这是一个问题:

您的机器可能有多个内核/处理器。多核允许多个线程同时执行,即多个线程可以在任何特定时刻执行。. 但是由于解释器由单个线程持有,其他线程即使可以访问核心也不会做任何事情。因此,您没有获得多核提供的任何优势,因为在任何时候都只使用一个核,即当前持有解释器的线程正在使用的核。因此,您的程序的执行时间与执行单线程程序一样长。

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

编辑:GIL 是 CPython 的实现细节。IronPython 和 Jython 没有 GIL,所以在它们中应该可以实现真正的多线程程序,以为我从未使用过 PyPy 和 Jython 并且不确定这一点。

于 2013-12-06T07:55:24.263 回答
24

Python 3.7 文档

我还想强调Pythonthreading文档中的以下引用:

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

这链接到词汇表条目,global interpreter lock其中解释了 GIL 意味着 Python 中的线程并行性不适合CPU 绑定任务

CPython 解释器用来确保一次只有一个线程执行 Python 字节码的机制。这通过使对象模型(包括关键的内置类型,如 dict)对并发访问隐式安全来简化 CPython 实现。锁定整个解释器使解释器更容易实现多线程,但代价是多处理器机器提供的大部分并行性。

然而,一些扩展模块,无论是标准的还是第三方的,都被设计成在执行压缩或散列等计算密集型任务时释放 GIL。此外,在执行 I/O 时总是释放 GIL。

过去创建“自由线程”解释器(以更精细的粒度锁定共享数据)的努力并未成功,因为在常见的单处理器情况下性能会受到影响。人们相信,克服这个性能问题将使实施更加复杂,因此维护成本更高。

这句话还暗示字典和变量赋值作为 CPython 实现细节也是线程安全的:

接下来,该包的文档multiprocessing解释了它如何通过生成过程来克服 GIL,同时暴露一个类似于的接口threading

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

并且解释它用作后端的文档:concurrent.futures.ProcessPoolExecutormultiprocessing

ProcessPoolExecutor 类是 Executor 子类,它使用进程池异步执行调用。ProcessPoolExecutor 使用多处理模块,这允许它绕过全局解释器锁,但也意味着只能执行和返回可提取对象。

这应该与使用线程而不是进程ThreadPoolExecutor的其他基类进行对比

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

从中我们得出结论,ThreadPoolExecutor它只适用于 I/O 密集型任务,同时ProcessPoolExecutor也可以处理 CPU 密集型任务。

下面的问题首先问为什么 GIL 存在:为什么是全局解释器锁?

进程与线程实验

Multiprocessing vs Threading Python中,我对 Python 中的进程与线程进行了实验分析。

快速预览结果:

在此处输入图像描述

于 2019-03-23T00:15:46.753 回答
20

Python 不允许真正意义上的多线程。它有一个多线程包,但是如果你想多线程来加速你的代码,那么使用它通常不是一个好主意。Python 有一个称为全局解释器锁 (GIL) 的构造。

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

GIL 确保在任何时候只有一个“线程”可以执行。一个线程获取 GIL,做一些工作,然后将 GIL 传递给下一个线程。这发生得非常快,因此在人眼看来,您的线程似乎是在并行执行,但实际上它们只是轮流使用相同的 CPU 内核。所有这些 GIL 传递都会增加执行的开销。这意味着如果你想让你的代码运行得更快,那么使用 threading 包通常不是一个好主意。

使用 Python 的线程包是有原因的。如果你想同时运行一些东西,效率不是问题,那完全没问题,很方便。或者,如果您正在运行需要等待某事(例如一些 IO)的代码,那么它可能会很有意义。但是线程库不会让你使用额外的 CPU 内核。

多线程可以外包给操作系统(通过执行多处理)、一些调用您的 Python 代码的外部应用程序(例如 Spark 或 Hadoop)或您的 Python 代码调用的一些代码(例如:您可以拥有您的 Python代码调用一个 C 函数来执行昂贵的多线程工作)。

于 2016-11-18T14:45:12.323 回答
16

每当两个线程可以访问同一个变量时,您就会遇到问题。例如,在 C++ 中,避免该问题的方法是定义一些互斥锁以防止两个线程同时进入一个对象的 setter。

多线程在 python 中是可能的,但是两个线程不能以比一条 python 指令更细的粒度同时执行。正在运行的线程正在获得一个名为 GIL 的全局锁。

这意味着如果您开始编写一些多线程代码以利用您的多核处理器,您的性能将不会提高。通常的解决方法包括多进程。

请注意,如果您在使用 C 语言编写的方法中,则可以释放 GIL。

GIL 的使用不是 Python 所固有的,而是它的一些解释器所固有的,包括最常见的 CPython。(#edited,见评论)

GIL 问题在 Python 3000 中仍然有效。

于 2009-08-18T14:57:06.930 回答
1

为什么 Python(CPython 和其他)使用 GIL

来自http://wiki.python.org/moin/GlobalInterpreterLock

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

如何从 Python 中删除它?

和 Lua 一样,也许 Python 可以启动多个 VM,但 python 没有这样做,我想应该还有其他一些原因。

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

于 2015-07-09T00:18:30.980 回答
1

我想分享一个来自视觉效果多线程一书中的例子。所以这是一个经典的死锁情况

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                      ║
╚═══╩════════════════════════════════════════╩══════════════════════════════════════╝
于 2015-11-03T06:50:24.907 回答