What is python's global interpreted lock (GIL)?

What is python's global interpreted lock (GIL)?

      What we call the Python Global Interpretation Lock (GIL) is simply a mutex (or lock), which allows only one thread to control the Python interpreter.

      This means that only one thread is executing at any one point in time. The GIL has little impact on programmers performing single-threaded tasks, but it becomes a performance bottleneck for CPU-bound and multithreaded tasks.

      Since the GIL only allows one thread to run at a time, even in a multithreaded framework with multiple CPU cores, it has a notorious reputation among Python's many features.

      In this article, you'll learn how the GIL affects the performance of your Python programs and how to mitigate its impact on your code.

What problem does the GIL solve in Python?

       Python uses reference counting for memory management, which means that objects created in Python have a reference counting variable that keeps track of the number of references to that object. When the number is 0, the memory occupied by the object is released.

      Let's demonstrate how reference counting works with a simple code:

      In the above example, the reference count of the empty list object [ ] is 3. The list object is referenced by a, b, and the arguments passed to sys.getrefcount( ). 

      Back to the GIL itself:

      The problem is that this reference count variable needs to be protected from race conditions when two threads increase or decrease at the same time. If this happens, it can result in leaked memory never being freed, or worse, by mistakenly freeing memory while a reference to an object still exists. This can crash the Python program or bring all kinds of weird bugs.

      By adding locks to data structures that are shared across threads so that data cannot be modified inconsistently, this is a great way to keep reference counted variables safe.

      But adding locks to each object or group of objects means that there will be multiple locks, which leads to another problem - deadlocks (which only happen when there are multiple locks). And another side effect is performance degradation due to repeatedly acquiring and releasing locks.

      The GIL is a single lock on the interpreter itself, and it adds a rule that any execution of Python bytecode needs to acquire the interpreter lock. This effectively prevents deadlocks (since there is only one lock) and doesn't introduce too much performance overhead. But it does make every computationally intensive task single-threaded.

      The GIL is also used by other language interpreters (such as Ruby), but this is not the only solution to this problem. Some programming languages ​​avoid the GIL's request for thread-safe memory management by using methods other than reference counting, such as garbage collection.

      On the other hand, this also means that these languages ​​often need to add other performance-enhancing features (such as JIT compilers) to make up for the loss of the GIL's single-threaded performance benefits.

Why choose GIL as the solution?

      So why is such a seemingly stumbling technology used in Python? Was this a bad decision by the Python developers?

      As Larry Hasting said, the design decision of the GIL is one of the big reasons why Python is so popular today.

      Python has been around since the time when operating systems had no concept of threads. Python was designed to be easy to use for faster development, which has led to more and more programmers using Python.

      Many extensions have been written to the C library for functionality that is required by Python. To prevent inconsistent changes, these C extensions require thread-safe memory management, which the GIL provides.

      The GIL is very easy to implement and easy to add to Python. Because only one lock needs to be managed, it brings performance improvements for single-threaded tasks.

      Non-thread-safe C libraries became easier to integrate, and these C extensions were one of the reasons why Python was embraced by different communities.

      As you can see, the GIL is a practical solution to difficult problems faced by CPython developers in their early Python careers.

Effects on multithreaded Python programs

      When you look at some typical Python programs or any computer program you will find that there is a difference in the performance of a program for computationally intensive and I/O intensive tasks.

      计算密集型任务是那些促使CPU达到极限的任务。这其中包括了进行数学计算的程序,如矩阵相乘、搜索、图像处理等。

      I/O密集型任务是一些需要花费时间来等待来自用户、文件、数据库、网络等的输入输出的任务。I/O密集型任务有时需要等待非常久直到他们从数据源获取到他们所需要的内容为止。这是因为在准备好输入输出之前数据源本身需要先进行自身处理。举例来说,一个用户考虑在输入提示中输入什么或者在其自己进程中运行的数据库查询。

      让我们先来看一个执行倒计时的简单的计算密集型程序:

      在我的4核系统上运行得到以下输出:

      接下来我对代码做出微调,使用两个线程并行处理来完成倒计时:

      接下来我再次运行:

      正如你所看到的,两个版本的完成时间相差无几。在多线程版本中GIL阻止了计算密集型任务线程并行执行。

      GIL对I/O密集型任务多线程程序的性能没有太大的影响,因为在等待I/O时锁可以在多线程之间共享。

      但是对于一个线程是完全计算密集型的任务来说(例如,利用线程进行部分图像处理)不仅会由于锁而变成单线程任务而且还会明显的增加执行时间。正如上例中多线程与完全单线程相比的结果。

      这种执行时间的增加是由于锁带来的获取和释放开销。


为什么GIL还没有被删除?

      Python的开发者收到了许许多多关于这方面的抱怨,但是像Python这样极受欢迎的语言无法做出去除GIL这样的巨变同时还不造成向后不兼容问题。

      GIL显然是可以被删除的,而且在过去这项任务也被开发者和研究人员多次完成。但是所有的尝试打破了在很大程度上取决于由GIL提供解决方案的C扩展市场。

      当然,还有许多其他解决方案可以解决GIL问题,但是其中一些以牺牲单线程和多线程I/O密集型任务的性能表现为代价,而另外一些解决方法又过于复杂。毕竟新版本发布后你不会希望你的Python跑得慢了些。

      BDFL of Python的创始人Guido van Rossum在2007年09月的文章《It isn’t Easy to remove the GIL》中向社区做出回答:

      “如果单线程任务和多线程I/O密集型任务的性能表现不会下降,那么我十分希望Py3k中能出现一组修补程序。”

      当然了,此后的每一次尝试都没有满足这个条件。

为什么在Python 3 中GIL没有被移除?

Python3中的确有机会使得许多功能从零开始,并且在这个过程中打破了那些需要更改和更新的C扩展并且将其移植到Python 3中。这也是为什么Python 3的早期版本被社区采纳的较慢的原因。

但是为什么GIL没有被删除?

      删除GIL会使得Python 3在处理单线程任务方面比Python 2慢,可以想像会产生什么结果。你不能否认GIL带来的单线程性能优势,这也就是为什么Python 3中仍然还有GIL。

      但是Python 3的确对现有GIL做了重大改进。

      我们仅仅讨论了GIL对“仅计算密集型任务”和“仅I/O密集型任务”的影响,但是对于那些一部分线程是计算密集型一部分线程是I/O密集型的程序来说会怎么样呢?

      在这样的程序中,Python的GIL通过不让I/O密集型线程从计算密集型线程获取GIL而使I/O密集型线程陷入瘫痪。

      这是因为Python中内嵌了一种机制,这个机制在固定连续使用时间后强迫线程释放GIL,并且如果没人获取这个GIL,那么同一线程可以继续使用。

      这个机制面临的问题是大多数计算密集型线程会在别的线程获取GIL之前再次获取GIL。这个研究工作由David Beazley进行,并且你可以在这里得到可视化资源。

       Antoine Pitrou于2009年在Python3.2中解决了这个问题,他添加了一种机制来查看其他线程请求GIL的访问数量,当数量下降时不允许当前线程在其他线程有机会运行之前重新获取GIL。

如何处理Python中的GIL?

      如果GIL给你带来困扰,你可尝试一下方法:

多进程vs多线程:最流行的方法是应用多进程方法,在这个方法中你使用多个进程而不是多个线程。每一个Python进程都有自己的Python解释器和内存空间,因此GIL不会成为问题。Python拥有一个multiprocessing模块可以帮助我们轻松创建多进程:

      在系统上运行得到

      相比于多线程版本,性能有所提升。

      但是时间并没有下降到我们之前版本的一半,这是因为进程管理有自己的开销。多进程比多线程更“重”,因此请记住,这可能成为规模瓶颈。

替代Python解释器:Python中有多个解释器实现办法,分别用C,Java,C#和Python编写的CPython,JPython,IronPython和PyPy是最受欢迎的。GIL只存在于传统的Python实现方法如CPython中。如果你的程序及其库文件可以通过别的实现方式实现,那么你也可以尝试一下。

等等看吧:许多用户利用GIL提升了单线程任务性能表现。当然多线程程序员们也不必为此烦恼,因为Python社区内的一些聪明大脑们正在致力于从CPython中删除GIL。其中一种尝试为Giletomy。


      Python GIL经常被认为是一个神秘而困难的话题。但是请记住作为一名Python支持者,只有当您正在编写C扩展或者您的程序中有计算密集型的多线程任务时才会被GIL影响。

      在这种情况下,这篇文章应该给了你需要的一切去了解GIL是什么以及如何在自己的项目中处理它。如果您希望了解GIL的低层次内部运行,我建议您观看David Beazley的Understanding the Python GIL。

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324421872&siteId=291194637