什么是全球解释器锁,为什么它是一个问题?
围绕从Python中删除GIL已经产生了很多噪音,我想知道为什么这么重要.我自己从未编写过编译器或解释器,所以不要节俭细节,我可能需要他们理解.
Python的GIL旨在从不同的线程序列化对解释器内部的访问.在多核系统上,这意味着多个线程无法有效地利用多个核心.(如果GIL没有导致这个问题,大多数人都不会关心GIL - 由于多核系统的普及,它只会被提出作为一个问题.)如果你想详细了解它,您可以观看此视频或查看此幻灯片组.这可能是太多的信息,但后来你确实要求详细信息:-)
请注意,Python的GIL只是CPython(参考实现)的一个问题.Jython和IronPython没有GIL.作为Python开发人员,除非您正在编写C扩展,否则通常不会遇到GIL.C扩展编写器需要在其扩展阻止I/O时释放GIL,以便Python进程中的其他线程有机会运行.
假设你有多个线程并没有真正触及彼此的数据.那些应该尽可能独立地执行.如果你有一个"全局锁定",你需要获取它来(比如说)调用一个函数,这可能最终成为一个瓶颈.你最终可以获得多线程的好处.
把它变成现实世界的比喻:想象100名开发人员只在一家咖啡杯里工作.大多数开发人员会花时间等待咖啡而不是编码.
这些都不是特定于Python的 - 我不知道Python首先需要GIL的细节.但是,希望它能让您更好地了解一般概念.
让我们首先了解python GIL提供的内容:
任何操作/指令都在解释器中执行.GIL确保解释器在特定时刻由单个线程持有.你的多线程python程序在一个解释器中工作.在任何特定时刻,该解释器由单个线程保持.这意味着,只有其持有的解释线程运行在任何时刻.
现在为什么这是一个问题:
您的计算机可能有多个核心/处理器.多核允许多个线程同时执行,即多个线程可以在任何特定时刻执行..但由于解释器由单个线程持有,因此其他线程即使可以访问核心也没有做任何事情.因此,您没有获得多个内核提供的任何优势,因为在任何时刻,只使用单个内核,即当前持有解释器的线程使用的内核.因此,您的程序将花费很长时间来执行,就像它是一个单线程程序一样.
但是,在GIL之外发生可能阻塞或长时间运行的操作,例如I/O,图像处理和NumPy数字运算.取自这里.因此,对于此类操作,尽管存在GIL,多线程操作仍将比单线程操作更快.因此,GIL并不总是瓶颈.
编辑:GIL是CPython的实现细节.IronPython和Jython没有GIL,所以一个真正的多线程程序应该可以在它们中,我以为我从来没有使用过PyPy和Jython,也不确定.
Python不允许在最真实的意义上使用多线程.它有一个多线程包,但如果你想多线程来加速你的代码,那么使用它通常不是一个好主意.Python有一个名为Global Interpreter Lock(GIL)的结构.
https://www.youtube.com/watch?v=ph374fJqFPE
GIL确保每次只能执行一个"线程".一个线程获取GIL,做一点工作,然后将GIL传递到下一个线程.这种情况很快发生,因此对于人眼看来,您的线程似乎并行执行,但它们实际上只是轮流使用相同的CPU核心.所有这些GIL传递都增加了执行的开销.这意味着如果您想让代码运行得更快,那么使用线程包通常不是一个好主意.
有理由使用Python的线程包.如果你想同时运行一些东西,效率不是一个问题,那么它就完全没问题了.或者,如果您正在运行需要等待某些事情的代码(例如某些IO),那么它可能会很有意义.但是线程库不会让你使用额外的CPU内核.
多线程可以外包到操作系统(通过多处理),一些调用Python代码的外部应用程序(例如,Spark或Hadoop),或者Python代码调用的一些代码(例如:你可以拥有你的Python)代码调用一个C函数来完成昂贵的多线程事务.
只要两个线程可以访问同一个变量,就会出现问题.例如,在C++中,避免问题的方法是定义一些互斥锁,以防止两个线程同时进入对象的setter.
python中可以进行多线程处理,但是两个线程不能同时以比一条python指令更精细的粒度执行.正在运行的线程正在获得一个名为GIL的全局锁.
这意味着如果您开始编写一些多线程代码以利用您的多核处理器,您的性能将无法提高.通常的解决方法包括进行多进程.
请注意,如果您在C中编写的方法中,可以释放GIL.
使用GIL不是Python固有的,而是它的一些解释器,包括最常见的CPython.(#edited,见评论)
GIL问题在Python 3000中仍然有效.
Python 3.7文档
我还要强调Python threading
文档中的以下引号:
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是一个程序包,它使用类似于线程模块的API支持生成过程。多处理程序包提供本地和远程并发,通过使用子进程而不是线程来有效地避开全局解释器锁。因此,多处理模块允许程序员充分利用给定机器上的多个处理器。它可以在Unix和Windows上运行。
以及用于concurrent.futures.ProcessPoolExecutor
解释该文档multiprocessing
用作后端的文档:
ProcessPoolExecutor类是Executor子类,它使用进程池异步执行调用。ProcessPoolExecutor使用多处理模块,该模块可以使其避开全局解释器锁,但也意味着只能执行和返回可拾取对象。
应对比于其他基类ThreadPoolExecutor
的是使用线程而不是进程
ThreadPoolExecutor是一个Executor子类,它使用线程池异步执行调用。
从中我们得出结论,ThreadPoolExecutor
它仅适用于I / O绑定的任务,同时ProcessPoolExecutor
还可以处理CPU绑定的任务。
下面的问题询问为什么GIL首先存在:为什么使用全局解释器锁定?
进程与线程实验
在Multiprocessing vs Threading Python中,我对Python中的进程与线程进行了实验分析。
快速预览结果: