CPython 中的全局解释器锁 (GIL) 是什么?
- 2024-11-25 08:49:00
- admin 原创
- 169
问题描述:
什么是全局解释器锁?为什么它是一个问题?
关于从 Python 中删除 GIL 的争论已经很多,我想了解为什么这如此重要。我自己从未编写过编译器或解释器,所以不要吝惜细节,我可能需要它们来理解。
解决方案 1:
Python 的 GIL 旨在序列化从不同线程对解释器内部的访问。在多核系统上,这意味着多个线程无法有效利用多个内核。(如果 GIL 没有导致这个问题,大多数人就不会关心 GIL - 它之所以被提出来只是因为多核系统越来越流行。)如果您想详细了解它,可以观看此视频或查看这组幻灯片。信息量可能太多了,但您确实要求提供详细信息 :-)
请注意,Python 的 GIL 实际上只是 CPython(参考实现)的问题。Jython 和 IronPython 没有 GIL。作为 Python 开发人员,除非您正在编写 C 扩展,否则您通常不会遇到 GIL。C 扩展编写者需要在其扩展执行阻塞 I/O 时释放 GIL,以便 Python 进程中的其他线程有机会运行。
解决方案 2:
假设您有多个线程,它们实际上并不接触彼此的数据。这些线程应该尽可能独立执行。如果您有一个“全局锁”,需要获取它才能(比如说)调用某个函数,那么这可能会成为瓶颈。一开始,您可能不会从拥有多个线程中获得太多好处。
用现实世界来类比:想象一下,一家公司有 100 名开发人员,但只有一个咖啡杯。大多数开发人员会花时间等待咖啡,而不是编码。
这些都不是 Python 独有的——我一开始就不知道 Python 需要 GIL 的具体用途。不过,希望它能让你更好地了解一般概念。
解决方案 3:
我们先来了解一下python GIL提供了什么:
任何操作/指令都在解释器中执行。GIL 确保解释器在特定时刻由单个线程持有。并且具有多个线程的 Python 程序在单个解释器中运行。在任何特定时刻,此解释器都由单个线程持有。这意味着在任何时刻,只有持有解释器的线程在运行。
那么为什么这是一个问题:
您的机器可能有多个内核/处理器。多个内核允许多个线程同时执行,即多个线程可以在任何特定时刻执行。但由于解释器由单个线程控制,因此其他线程即使可以访问内核也不会执行任何操作。因此,您无法获得多核带来的任何优势,因为在任何时刻都只有一个内核被使用,即当前控制解释器的线程正在使用的核心。因此,您的程序将像单线程程序一样花费很长时间来执行。
但是,潜在的阻塞或长时间运行的操作(例如 I/O、图像处理和 NumPy 数字运算)发生在 GIL 之外。摘自此处。因此,对于此类操作,尽管存在 GIL,多线程操作仍将比单线程操作更快。因此,GIL 并不总是瓶颈。
编辑:GIL 是 CPython 的一个实现细节。IronPython 和 Jython 没有 GIL,因此在它们中应该可以实现真正的多线程程序,不过我从未使用过 PyPy 和 Jython,对此也不太确定。
解决方案 4:
Python 3.7 文档
我还想强调一下Pythonthreading
文档中的以下引文:
CPython 实现细节:在 CPython 中,由于存在全局解释器锁,因此一次只能有一个线程执行 Python 代码(尽管某些性能导向型库可能会克服此限制)。如果您希望应用程序更好地利用多核机器的计算资源,建议您使用
multiprocessing
或concurrent.futures.ProcessPoolExecutor
。但是,如果您想同时运行多个 I/O 密集型任务,线程仍然是一个合适的模型。
这链接到词汇表条目global interpreter lock
,其中解释了 GIL 意味着 Python 中的线程并行不适合CPU 密集型任务:
CPython 解释器使用的机制,用于确保一次只有一个线程执行 Python 字节码。通过使对象模型(包括关键内置类型,如 dict)隐式安全地防止并发访问,这简化了 CPython 实现。锁定整个解释器使解释器更容易实现多线程,但代价是多处理器机器所能提供的大部分并行性。
但是,一些扩展模块(无论是标准模块还是第三方模块)的设计使得在执行压缩或散列等计算密集型任务时释放 GIL。此外,执行 I/O 时始终会释放 GIL。
过去创建“自由线程”解释器(以更精细的粒度锁定共享数据的解释器)的努力没有成功,因为在常见的单处理器情况下性能会受到影响。人们认为,克服这一性能问题将使实现变得更加复杂,因此维护成本更高。
这句话还暗示,作为 CPython 实现细节,字典以及变量赋值也是线程安全的:
Python 变量赋值是原子的吗?
Python 词典中的线程安全
接下来,该包的文档multiprocessing
解释了它如何通过生成进程并公开类似于的接口来克服 GIL threading
:
multiprocessing 是一个支持使用类似于 threading 模块的 API 生成进程的软件包。multiprocessing 软件包提供本地和远程并发,通过使用子进程而不是线程有效地避开了全局解释器锁。因此,multiprocessing 模块允许程序员充分利用给定机器上的多个处理器。它可以在 Unix 和 Windows 上运行。
并且文档concurrent.futures.ProcessPoolExecutor
解释了它用作multiprocessing
后端:
ProcessPoolExecutor 类是 Executor 的子类,它使用进程池来异步执行调用。ProcessPoolExecutor 使用多处理模块,这使其能够避开全局解释器锁,但也意味着只能执行和返回可 picklable 对象。
这与使用线程而不是进程的ThreadPoolExecutor
其他基类形成对比
ThreadPoolExecutor 是 Executor 的子类,它使用线程池来异步执行调用。
由此我们得出结论,它只ThreadPoolExecutor
适合 I/O 密集型任务,同时ProcessPoolExecutor
也可以处理 CPU 密集型任务。
进程与线程实验
在多处理与线程 Python中,我对 Python 中的进程与线程进行了实验分析。
快速预览结果:
其他语言
这个概念似乎也存在于 Python 之外,同样适用于 Ruby,例如:https://en.wikipedia.org/wiki/Global_interpreter_lock
它提到了以下优点:
提高单线程程序的速度(不需要分别获取或释放所有数据结构的锁),
轻松集成通常非线程安全的 C 库,
易于实现(单个 GIL 比无锁解释器或使用细粒度锁的解释器更容易实现)。
但是 JVM 似乎在没有 GIL 的情况下也能很好地工作,所以我想知道它是否值得。以下问题是 GIL 存在的原因:为什么要有全局解释器锁?
解决方案 5:
Python 不支持真正意义上的多线程。它有一个多线程包,但是如果你想要使用多线程来加快代码速度,那么使用它通常不是一个好主意。Python 有一个称为全局解释器锁 (GIL) 的构造。
https://www.youtube.com/watch?v=ph374fJqFPE
GIL 确保一次只能执行一个“线程”。一个线程获取 GIL,执行一些工作,然后将 GIL 传递给下一个线程。这个过程发生得非常快,所以在人眼看来,您的线程似乎在并行执行,但实际上它们只是轮流使用同一个 CPU 核心。所有这些 GIL 传递都会增加执行开销。这意味着,如果您想让代码运行得更快,那么使用线程包通常不是一个好主意。
使用 Python 的线程包是有原因的。如果你想同时运行一些任务,并且效率不是问题,那么它完全没问题,而且很方便。或者,如果你正在运行需要等待某些任务(例如某些 IO)的代码,那么它可能很有意义。但是线程库不会让你使用额外的 CPU 核心。
多线程可以外包给操作系统(通过执行多处理)、调用 Python 代码的某些外部应用程序(例如 Spark 或 Hadoop)或 Python 代码调用的某些代码(例如:您可以让 Python 代码调用执行昂贵的多线程操作的 C 函数)。
解决方案 6:
每当两个线程访问同一个变量时,就会出现问题。例如,在 C++ 中,避免此问题的方法是定义一些互斥锁,以防止两个线程同时进入某个对象的设置器。
Python 中可以实现多线程,但两个线程无法同时执行,粒度不能小于一条 Python 指令。正在运行的线程正在获取一个称为 GIL 的全局锁。
这意味着如果你开始编写一些多线程代码以利用多核处理器,你的性能将不会提高。通常的解决方法是使用多进程。
请注意,如果您处于用 C 编写的方法中,则可以释放 GIL。
GIL 的使用不是 Python 所固有的,而是它的一些解释器所固有的,包括最常见的 CPython。(#edited,见评论)
GIL 问题在 Python 3000 中仍然存在。
解决方案 7:
为什么 Python(CPython 和其他)使用 GIL
来自http://wiki.python.org/moin/GlobalInterpreterLock
在 CPython 中,全局解释器锁(GIL)是一个互斥锁,用于阻止多个本机线程同时执行 Python 字节码。此锁是必需的,主要是因为 CPython 的内存管理不是线程安全的。
如何从 Python 中删除它?
像Lua一样,也许Python可以启动多个VM,但是python没有这样做,我猜应该还有其他原因。
在Numpy或者其他python扩展库中,有时候将GIL释放到其他线程可以提高整个程序的效率。
解决方案 8:
我想分享一个来自《视觉效果的多线程》一书的例子。这是一个典型的死锁情况
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 ║
╚═══╩════════════════════════════════════════╩══════════════════════════════════════╝