CPython中的全局解释器锁(GIL)是什么?

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

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

74268 次浏览

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

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

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

Python的GIL旨在序列化不同线程对解释器内部的访问。在多核系统上,这意味着多线程不能有效地利用多核。(如果GIL没有导致这个问题,大多数人都不会关心GIL——它只是作为一个问题被提出,因为多核系统越来越流行。)如果你想详细了解它,你可以查看这个视频这组幻灯片。这可能是太多的信息,但你确实问了细节:-)

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

当两个线程访问同一个变量时,你就有问题了。 例如,在c++中,避免这个问题的方法是定义一些互斥锁来防止两个线程,比如说,同时进入一个对象的setter

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

这意味着如果你开始写一些多线程代码来利用你的多核处理器,你的性能不会提高。 通常的解决方案包括使用multiprocess。

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

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

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

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

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

为什么这是个问题呢?

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

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

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

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

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 threading文档中的以下引用:

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

这个链接到global interpreter lock的术语表项,它解释了GIL意味着Python中的线程并行不适合CPU绑定任务:

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

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

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

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

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

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

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

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

它应该与另一个基类ThreadPoolExecutor相比,即使用线程而不是进程

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

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

进程与线程实验

多处理vs线程Python中,我做了一个关于Python中的进程与线程的实验分析。

快速预览结果:

enter image description here

其他语言

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

它提到了优点:

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

但是JVM似乎没有GIL也能做得很好,所以我想知道这样做是否值得。下面的问题问为什么GIL首先存在