什么是全局解释器锁,为什么它是一个问题?
有很多关于从Python中删除GIL的讨论,我想了解为什么这样做非常重要。我自己从未编写过编译器或解释器,因此请提供详细信息,以便我能够理解。
什么是全局解释器锁,为什么它是一个问题?
有很多关于从Python中删除GIL的讨论,我想了解为什么这样做非常重要。我自己从未编写过编译器或解释器,因此请提供详细信息,以便我能够理解。
Python的全局解释器锁(GIL)旨在序列化不同线程对解释器内部的访问。在多核系统中,这意味着多个线程无法有效地利用多个核心。(如果GIL没有导致这个问题,大多数人也不会关心GIL——只是由于多核系统越来越普遍,才将其提升为一个问题)。如果你想详细了解它,可以观看这个视频或查看这组幻灯片。可能有点太多信息了,但你确实要求了细节 :-)
请注意,Python的GIL只是针对参考实现CPython的一个问题。Jython和IronPython没有GIL。作为Python开发人员,你通常不会遇到GIL,除非你正在编写C扩展。C扩展编写者需要在其扩展进行阻塞I/O时释放GIL,以便Python进程中的其他线程有机会运行。
regex
、lxml
和numpy
模块。Cython允许在自定义代码中释放GIL,例如b2a_bin(data)
。 - jfs首先让我们理解Python GIL提供了什么:
所有操作和指令都是在解释器中执行的。GIL确保解释器在特定瞬间只由一个线程持有。而您的带有多个线程的Python程序在单个解释器中工作。在任何特定瞬间,这个解释器只被一个线程持有。这意味着只有持有解释器的线程在任何时刻运行。
那么这为什么是个问题:
您的计算机可能有多个内核/处理器。多个内核允许多个线程同时执行,即多个线程可以在任何特定时间执行。但由于解释器只被单个线程持有,即使其他线程可以访问内核,它们也不做任何事情。因此,您没有获得由多个内核提供的任何优势,因为在任何瞬间,只有一个内核正在使用,即当前持有解释器的线程正在使用的内核。所以,您的程序将花费与单线程程序相同的时间来执行。
然而,潜在的阻塞或长时间运行的操作,比如I/O、图像处理和NumPy数字处理,发生在GIL之外。摘自这里。因此,对于这样的操作,尽管存在GIL,多线程操作仍然比单线程操作快。所以,GIL并不总是瓶颈。
编辑: GIL是CPython的实现细节。IronPython和Jython没有GIL,因此在它们中可以实现真正的多线程程序,但我从未使用过PyPy和Jython,也不确定是否有效。
Python 3.7文档
我还想强调一下Python threading
文档中的以下引用:
CPython实现细节:由于全局解释器锁(Global Interpreter Lock),在CPython中,仅有一个线程可以执行Python代码(尽管某些面向性能的库可能会克服此限制)。如果您希望应用程序更好地利用多核计算机的计算资源,则建议使用
multiprocessing
或concurrent.futures.ProcessPoolExecutor
。但是,如果您想同时运行多个I/O绑定任务,则线程仍然是适当的模型。
这链接到“全局解释器锁”词汇表条目,其中解释了GIL意味着Python中的线程并行不适用于CPU绑定任务:
CPython解释器使用的机制,以确保只有一个线程在任何时候执行Python字节码。这使得CPython实现更加简单,通过隐式地使对象模型(包括关键的内置类型,如dict)安全免受并发访问的影响。锁定整个解释器使得解释器更容易成为多线程的,但牺牲了多处理器机器提供的大部分并行性。接下来,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中的多进程与多线程方面,我进行了一项实验性分析。
结果快速预览:
在其他语言中
这个概念似乎不仅存在于Python中,同样适用于Ruby等编程语言:https://en.wikipedia.org/wiki/Global_interpreter_lock
它提到了以下优点:
但是JVM似乎没有GIL也能正常运行,所以我想知道是否值得这样做。下面的问题问GIL首先存在的原因:Why the Global Interpreter Lock?
Python在真正意义上不允许多线程。它有一个多线程包,但如果你想要使用多线程来加速代码运行,通常不建议使用它。Python有一个叫做全局解释器锁(GIL)的结构。
https://www.youtube.com/watch?v=ph374fJqFPE
GIL确保只能有一个“线程”在任何时候执行。一个线程获取GIL,做一些工作,然后将GIL传递给下一个线程。这个过程非常快速,所以对于人眼来说,看起来好像你的线程在并行执行,但实际上它们只是轮流使用同一个CPU核心。所有这些GIL传递会增加执行的开销。这意味着,如果你想让你的代码运行得更快,那么经常使用线程包可能不是一个好主意。
使用Python的线程包也有一些优点。如果你想同时运行某些东西,并且效率不是一个问题,那么完全可以使用它。或者如果你正在运行需要等待某些东西(如一些IO)的代码,那么使用它可能是很有意义的。但是线程库不能让你使用额外的CPU核心。
多线程可以通过操作系统(通过多进程),一些调用你的Python代码的外部应用程序(例如Spark或Hadoop),或者你的Python代码调用的一些代码(例如:你可以让你的Python代码调用一个执行昂贵的多线程操作的C函数)来进行外包。
我想分享一本名为《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 ║
╚═══╩════════════════════════════════════════╩══════════════════════════════════════╝
为什么Python(包括CPython和其他版本)使用GIL
来源于http://wiki.python.org/moin/GlobalInterpreterLock
在CPython中,全局解释器锁(GIL)是一个互斥锁,它防止多个本地线程同时执行Python字节码。这个锁主要是必需的,因为CPython的内存管理不是线程安全的。
如何从Python中删除它?
像Lua一样,也许Python可以启动多个VM,但Python没有这样做,我猜应该有其他原因。
在Numpy或其他一些Python扩展库中,有时将GIL释放给其他线程可以提高整个程序的效率。
multiprocessing
包启动多个虚拟机。 - undefined