Python中的全局解释器锁(GIL)

原文地址:What is the Python Global Interpreter Lock (GIL)
Python全局解释器锁:简单来说,就是只允许Python解释器任意时刻只能有一个线程处于运行状态


1.GIL解决了Python的什么问题?
Python使用引用计数管理内存
举个栗子

>>>import sys
>>> a = []
>>> b = a
>>> sys.getrefcount(a)
3

回到GIL,引用变量在存在两个以上的线程同时增加或减少变量值得情形中,需要保护。如果没有相关机制做到这一点

将会导致未释放的内存泄漏,或者更糟糕的时,当一个对象的引用仍然存在时,致使释放内存出错。,这会使你的Python程序崩溃,或者产生一些奇怪的bugs.

如何解决上面问题:

通过对进程中共享的所有数据结构加锁,这样他们就不会不停的改变,从而保护了引用变量的安全。给每个对象或多个对象添加锁意味着多种锁的存在,这样会导致另外一个问题----Deadlocks(死锁,如果超过一把锁,可能会发生死锁),另一个副作用是由于重复获取和释放锁会导致性能下降。

GIL是解释器上的一个单锁,它为任何需要获取解释器锁的Python程序的执行制定了一个规则。因为是单锁,所以不存在死锁问题,也不会有太多的性能开销。最关键的是,它使得任何 受CPU限制的Python的程序都是单线程的。

GIL虽然被解释器用于其他语言(如Ruby),但并不是解决此问题的唯一方法。 有些语言通过使用除引用计数之外的方法(例如垃圾收集)来避免GIL对线程安全内存管理的要求。

另一方面,这意味着这些语言通常需要通过添加其他性能提升功能(如JIT编译器)来弥补GIL单线程性能优势的损失。

 

2.为什么选择GIL作为解决方案?

事实上,GIL的设计是让Python变得如此流行的一个重要原因。

Python诞生的时候,那个时候的操作系统并没有线程的概念,Python的目的就是易用,加速开发,这样的设计受到很多人的欢迎,越来越多的开发者开始使用Python。

Python的许多使用的特征都是由已存在的C语言库扩展而写的,为了防止不一致的更改,这些C的扩展需要GIL提供的线程安全内存管理。GIL在Python中很容易实现,可以提高对于只需要一个锁管理的单线程程序的性能。非线程安全的C库变得更容易集成。 这些C扩展成为不同社区容易采用Python的原因之一。

正如您所看到的,GIL是一个实用的解决方案,可以解决CPython开发人员在Python生命中早期遇到的一个难题。

 

3.多线程Python程序的影响

当你关注一个典型的Python程序时(或者其他任何电脑程序),那些性能上受CPU限制的和受I/O限制的程序存在差异。受CPU限制的程序在执行数学计算(如矩阵乘法),搜索,图像处理等程序时,会致使CPU的利用接近极限。而受I/O限制的程序仅仅需要等待来自用户、文件、数据库、网络的输入输出。有时候由于信息源在输入输出的时候需要进行一些处理,程序可能会长时间等待直到等到它需要的数据。

下面我们来看一个简单的受CPU限制的程序:

#single_threaded.py
import time
from threading import Thread

COUNT = 50000000

def countdown(n):
    while n>0:
        n -= 1

start = time.time()
countdown(COUNT)
end = time.time()

print('Time taken in seconds ---',end-start)


'Time taken in seconds -', 2.297261953353882

现在更改一下代码使用两个并行线程:

# multi_threaded.py
import time
from threading import Thread

COUNT = 50000000

def countdown(n):
    while n >0:
        n -= 1

t1 = Thread(target=countdown,args = (COUNT//2,))
t2 = Thread(target=countdown,args = (COUNT//2,))

start = time.time()
t1.start()
t2.start()
t1.join()
t2.join()
end = time.time()

print('Time taken in seconds ---',end-start)

'Time taken in seconds -', 2.4209160804748535

可以看到,两个版本所完成任务所花的时间差不多,在第二个版本(multi_threaded)GIL阻止了受CPU限制的线程并发

GIL对于I/O多线程程序的性能没有多大影响,因为在等待I/O的数据时,锁可以在线程之间共享。

但对于一个完全受CPU限制的程序,例如图像处理程序的线程,由于锁的原因,不仅会变成单线程,还会看到执行时间增长,跟上面的例子一样。导致增长的原因是获取和释放锁增加了开销。

 

4.为什么GIL还没移除?

Python开发者听到了太多关于GIL为什么不移除的抱怨,但Python作为种流行的语言,不能随便做出类似移除GIL这样的重大改变,会导致很多兼容性问题。

过去,也有很多开发者和研究者多次尝试想移除GIL,但是这样会打破现有的C扩展,这些扩展很大程度上依赖于GIL提供的解决方案。当然,也有其他方案解决这个问题,只是这些方案中会有一些会致使单线程和受I/O限制的多线程的性能下降,y一些会很难实施,毕竟,我们都不希望新版本的Python程序比现有的程序更慢,对吧?

看Python之父在文章 It isn't Easy to Remove the GIL 提出了他的看法

 

5.为什么Python3中不移除GIL?

Python3是有机会集成很多scratch的特征以及摒弃一些在Python3中使用需要更新和导入的C扩展。这也是早期的Python3版本在社区中难以流行起来的原因。

为什么GIL冰没有被删除?

删除GIL会使Python3在单线程的性能上不如Python2,GIL的单线程性能你无法争论,结果就是Python3中依然有GIL

相反,Python3给GIL带来了一个重要的改进。

我们讨论的是GIL对仅受CPU限制的和仅受I/O限制的多线程程序的影响,那么如果程序的一部分是I/O受限,一部分是CPU受限的呢?

在这样的程序中,已知Python的GIL会使I / O绑定的线程匮乏,因为它们没有机会从CPU绑定的线程中获取GIL。

这是因为Python内置的一种机制迫使线程在连续使用的固定间隔后释放GIL,并且如果没有其他人获得GIL,则相同的线程可以继续使用它。

这种机制的问题在于,大多数情况下,CPU绑定线程会在其他线程获取GIL之前重新获取GIL。这个问题在2009年由Antoine Pitrou在Python 3.2中得到修复,他添加了一种机制()以查看被删除的其他线程的GIL获取请求数,并且在其他线程有机会运行之前不允许当前线程重新获取GIL。

6.如何处理GIL?

如果GIL给你制造了麻烦,你可以尝试下面一些方法:

多进程 vs 多线程

如果你不使用线程而是使用多个进程,那么多进程更合适。每个Python进程会得到自己的Python解释器和内存空间,GIL不会成为麻烦。Python有多进程处理模块可以让我们很轻松的创建进程:

例如

from multiprocessing import Pool
import time

COUNT = 50000000
def countdown(n):
    while n > 0:
        n -= 1

if __name__ == "__main__":
    pool = Pool(processes = 2)
    start = time.time()
    r1 = pool.apply_async(countdown,[COUNT//2])
    r2 = pool.apply_async(countdown,[COUNT//2])
    pool.close()
    pool.join()
    end = time.time()
    print('Time taken in seconds ---',end-start)

'Time taken in seconds ---', 1.7674751281738281

对比与多线程的版本,这个性能有很大的提升。

至于时间为什么没有下降到一半,是因为多进程管理具有自己的开销。多进程的开销比多线程的开销大,这可能会成为缩放瓶颈

替代Python解释器:

Python有很多的解释器工具。CPython,Jython,IronPython以及PyPy,分别由C,Java,c#,和Python写的。GIL仅仅存在于最原始的Python实现版本CPython.所以,你的程序及其库可以通过其他解释器实现,可以尝试一下它们。

等:

虽然许多Python用户利用了GIL的单线程性能优势。多线程程序员不必烦恼,因为Python社区中一些最聪明的人正在努力从CPython中删除GIL。 一种这样的尝试被称为Gilectomy。

Python GIL通常被认为是一个神秘而困难的话题。 但请记住,作为Pythonista,如果您正在编写C扩展或者在程序中使用CPU绑定的多线程,则通常只会受到它的影响。

<think>好的,我现在需要向用户解释Python全局解释器锁GIL)。首先,我得确保自己正确理解GIL的概念和作用。GILPython中的一个机制,尤其是在CPython解释器中,用来同步线程的执行。我记得它的主要作用是确保同一时刻只有一个线程执行Python字节码,这有助于简化内存管理,但也可能影响多线程程序的性能。 接下来,我需要考虑用户可能的背景。用户可能有一定的编程经验,但对Python内部机制不太熟悉。所以,解释时要避免过于技术化,用通俗易懂的语言。同时,用户可能想知道GIL存在的原因、带来的影响以及如何应对这些问题。 首先,我应该从GIL的定义开始,明确它是什么。然后解释为什么Python需要GIL,比如因为CPython的内存管理不是线程安全的,GIL通过阻止多线程同时执行来防止竞争条件。这里要提到引用计数等细节,但不要过于深入,保持简洁。 接下来,要讨论GIL带来的影响。特别是多线程程序在CPU密集型任务上的性能问题,比如多线程并不能充分利用多核CPU,甚至可能比单线程更慢。但同时,对于I/O密集型任务,GIL的影响较小,因为线程在等待I/O时会释放GIL,允许其他线程运行。这里可能需要举例子,比如网络请求或文件操作属于I/O密集型,而数值计算属于CPU密集型。 然后,要探讨绕过或替代GIL的方法。比如使用多进程代替多线程,每个进程有自己的解释器和内存空间,这样就能利用多核。或者使用其他Python实现如Jython或IronPython,它们没有GIL。此外,可以提到用C扩展来处理性能关键部分,因为C扩展可以释放GIL。 还要考虑用户可能存在的疑问,比如为什么Python不移除GIL。这里需要解释历史原因和兼容性问题,移除GIL会破坏现有C扩展,而且实现起来复杂,影响单线程性能。虽然有过尝试,比如Python 3.2的GIL优化,但完全移除仍然困难。 最后,总结GIL的优缺点,并给出实际应用中的建议。比如在CPU密集型任务中使用多进程或C扩展,I/O密集型任务仍可用多线程。同时,提及异步编程模型如asyncio作为替代方案。 需要检查是否有遗漏点,比如GIL的具体工作机制,如何切换线程,以及不同Python版本对GIL的改进。另外,确保使用正确的术语,避免错误,比如区分CPython和其他实现。还要注意格式,数学表达式用$...$,独立公式用$$...$$,但当前内容可能不涉及数学公式,所以可能不需要。但如果有方程式,要正确应用格式。 另外,用户要求用中文回答,结构清晰,分点说明。可能需要分点列出GIL的作用、影响、解决方案等。使用例子帮助理解,比如多线程计算斐波那契数列的效率问题,或者多进程的multiprocessing模块。 需要确保内容准确,比如GIL确实存在于CPython,而其他实现如Jython没有。同时,在提到多进程时,指出其缺点,比如进程间通信的开销。还要提到concurrent.futures模块的使用,作为替代方案。 最后,检查是否有误解,比如GIL对多线程的影响是否绝对,或者是否所有Python程序都受GIL限制。确认GIL只影响CPU密集型多线程,而不影响I/O密集型,也不影响多进程。同时,确认Python中多线程依然适合I/O密集型任务,因为等待期间释放GIL。 总结思考后,组织内容结构,确保逻辑连贯,从定义、原因、影响、解决方案到总结,逐步展开,帮助用户全面理解GIL及其应对策略。</think>Python全局解释器锁(**Global Interpreter Lock, GIL**)是 CPython 解释器(Python 官方实现)中的一个关键机制,它对多线程编程有深远影响。以下是逐步解析: --- ### 1. **GIL 是什么?** - GIL 是一个**互斥**(Mutex),保证同一时刻只有一个线程能执行 Python 字节码。 - 无论 CPU 有多少核心,**多线程 Python 程序在 CPython 中无法并行执行**,只能交替执行(并发)。 --- ### 2. **为什么需要 GIL?** - **简化内存管理**:Python 使用引用计数管理对象生命周期。若多线程同时修改引用计数,可能导致内存错误。GIL 通过单线程执行避免竞争条件。 - **保护 C 扩展兼容性**:许多 Python C 扩展依赖 GIL线程安全假设,移除 GIL 会破坏现有生态。 --- ### 3. **GIL 的影响** #### ✅ 优点 - 简化单线程程序的实现。 - 提高单线程性能(避免多线程竞争开销)。 #### ❌ 缺点 - **CPU 密集型任务性能瓶颈**:多线程无法利用多核加速。例如: ```python # 计算斐波那契数列的多线程代码并不会更快 import threading def compute(): result = 0 for _ in range(10**7): result += 1 # 两个线程同时执行 t1 = threading.Thread(target=compute) t2 = threading.Thread(target=compute) t1.start(); t2.start() t1.join(); t2.join() ``` 由于 GIL,两个线程实际是交替执行,总耗时可能接近单线程的两倍。 - **I/O 密集型任务影响较小**:线程在等待 I/O(如网络请求、文件读写)时会主动释放 GIL,其他线程可继续执行。 --- ### 4. **如何绕过 GIL?** #### 方法 1:使用多进程 - 每个进程有独立的 Python 解释器和内存空间,可绕过 GIL。 - 通过 `multiprocessing` 模块或 `concurrent.futures.ProcessPoolExecutor` 实现并行。 ```python from multiprocessing import Pool def compute(n): return sum(range(n)) with Pool(4) as p: # 4 进程并行 print(p.map(compute, [10**7]*4)) ``` #### 方法 2:使用其他 Python 实现 - **Jython/IronPython**:基于 JVM/.NET 的实现,无 GIL,但生态兼容性差。 - **PyPy**:通过 JIT 优化性能,但仍保留 GIL。 #### 方法 3:C/C++ 扩展 - 在 C 扩展中手动释放 GIL:使用 `Py_BEGIN_ALLOW_THREADS` 和 `Py_END_ALLOW_THREADS` 宏。 - 常用库如 NumPy、Cython 已在底层规避 GIL。 --- ### 5. **GIL 的未来** - **短期内不会消失**:移除 GIL 需重写 CPython 内存管理并破坏现有 C 扩展,代价巨大。 - **优化方向**:Python 3.2+ 通过更细粒度的(如 `PyMutex`)减少 GIL 的负面作用,但未根治问题。 --- ### 总结 - **适用场景**: - 多线程适合 I/O 密集型任务(如网络爬虫)。 - 多进程或异步编程(如 `asyncio`)适合 CPU 密集型任务。 - **开发建议**:优先使用多进程、异步编程或 C 扩展处理高性能需求,而非依赖多线程
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值