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

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


当前回答

Python 3.7文档

我还想强调Python线程文档中的以下引用:

CPython implementation detail: In CPython, due to the Global Interpreter Lock, only one thread can execute Python code at once (even though certain performance-oriented libraries might overcome this limitation). If you want your application to make better use of the computational resources of multi-core machines, you are advised to use multiprocessing or concurrent.futures.ProcessPoolExecutor. However, threading is still an appropriate model if you want to run multiple I/O-bound tasks simultaneously.

这个链接指向全局解释器锁的Glossary条目,它解释了GIL意味着Python中的线程并行不适合CPU绑定的任务:

The mechanism used by the CPython interpreter to assure that only one thread executes Python bytecode at a time. This simplifies the CPython implementation by making the object model (including critical built-in types such as dict) implicitly safe against concurrent access. Locking the entire interpreter makes it easier for the interpreter to be multi-threaded, at the expense of much of the parallelism afforded by multi-processor machines. However, some extension modules, either standard or third-party, are designed so as to release the GIL when doing computationally-intensive tasks such as compression or hashing. Also, the GIL is always released when doing I/O. Past efforts to create a “free-threaded” interpreter (one which locks shared data at a much finer granularity) have not been successful because performance suffered in the common single-processor case. It is believed that overcoming this performance issue would make the implementation much more complicated and therefore costlier to maintain.

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

Python变量赋值是原子的吗? Python字典中的线程安全

接下来,多处理包的文档解释了它如何通过在暴露类似于线程的接口时生成进程来克服GIL:

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

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

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

它应该与使用线程而不是进程的其他基类ThreadPoolExecutor形成对比

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

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

进程与线程实验

在Multiprocessing vs Threading Python一文中,我对Python中的进程vs线程做了一个实验分析。

快速预览结果:

其他语言

这个概念似乎也存在于Python之外,同样适用于Ruby,例如:https://en.wikipedia.org/wiki/Global_interpreter_lock

它提到了优点:

提高单线程程序的速度(不需要分别获取或释放所有数据结构上的锁) 轻松集成通常不是线程安全的C库, 易于实现(使用单个GIL比使用无锁解释器或使用细粒度锁的解释器要简单得多)。

但是JVM似乎没有GIL也能做得很好,所以我想知道这样做是否值得。下面的问题问为什么GIL会存在:为什么是全局解释器锁?

其他回答

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

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

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

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

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

Python 3.7文档

我还想强调Python线程文档中的以下引用:

CPython implementation detail: In CPython, due to the Global Interpreter Lock, only one thread can execute Python code at once (even though certain performance-oriented libraries might overcome this limitation). If you want your application to make better use of the computational resources of multi-core machines, you are advised to use multiprocessing or concurrent.futures.ProcessPoolExecutor. However, threading is still an appropriate model if you want to run multiple I/O-bound tasks simultaneously.

这个链接指向全局解释器锁的Glossary条目,它解释了GIL意味着Python中的线程并行不适合CPU绑定的任务:

The mechanism used by the CPython interpreter to assure that only one thread executes Python bytecode at a time. This simplifies the CPython implementation by making the object model (including critical built-in types such as dict) implicitly safe against concurrent access. Locking the entire interpreter makes it easier for the interpreter to be multi-threaded, at the expense of much of the parallelism afforded by multi-processor machines. However, some extension modules, either standard or third-party, are designed so as to release the GIL when doing computationally-intensive tasks such as compression or hashing. Also, the GIL is always released when doing I/O. Past efforts to create a “free-threaded” interpreter (one which locks shared data at a much finer granularity) have not been successful because performance suffered in the common single-processor case. It is believed that overcoming this performance issue would make the implementation much more complicated and therefore costlier to maintain.

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

Python变量赋值是原子的吗? Python字典中的线程安全

接下来,多处理包的文档解释了它如何通过在暴露类似于线程的接口时生成进程来克服GIL:

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

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

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

它应该与使用线程而不是进程的其他基类ThreadPoolExecutor形成对比

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

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

进程与线程实验

在Multiprocessing vs Threading Python一文中,我对Python中的进程vs线程做了一个实验分析。

快速预览结果:

其他语言

这个概念似乎也存在于Python之外,同样适用于Ruby,例如:https://en.wikipedia.org/wiki/Global_interpreter_lock

它提到了优点:

提高单线程程序的速度(不需要分别获取或释放所有数据结构上的锁) 轻松集成通常不是线程安全的C库, 易于实现(使用单个GIL比使用无锁解释器或使用细粒度锁的解释器要简单得多)。

但是JVM似乎没有GIL也能做得很好,所以我想知道这样做是否值得。下面的问题问为什么GIL会存在:为什么是全局解释器锁?

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

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

为什么这是个问题呢?

Your machine could be having multiple cores/processors. And multiple cores allow multiple threads to execute simultaneously i.e multiple threads could execute at any particular instant of time.. But since the interpreter is held by a single thread, other threads are not doing anything even though they have access to a core. So, you are not getting any advantage provided by multiple cores because at any instant only a single core, which is the core being used by the thread currently holding the interpreter, is being used. So, your program will take as long to execute as if it were a single threaded program.

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

编辑:GIL是CPython的一个实现细节。IronPython和Jython没有GIL,所以一个真正的多线程程序应该是可能的,虽然我从来没有使用过PyPy和Jython,不确定这一点。

我想分享一个书中的例子多线程的视觉效果。这就是典型的死锁情况

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                      ║
╚═══╩════════════════════════════════════════╩══════════════════════════════════════╝

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

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

这些都不是Python特有的——我不知道Python最初需要GIL做什么。不过,希望这能让你们更好地理解这个概念。