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

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


当前回答

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

从http://wiki.python.org/moin/GlobalInterpreterLock

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

如何从Python中删除它?

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

在Numpy或其他一些python扩展库中,有时,将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会存在:为什么是全局解释器锁?

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

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

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

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

从http://wiki.python.org/moin/GlobalInterpreterLock

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

如何从Python中删除它?

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

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

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

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