Python中GIL锁

GIL并不是Python的特性,Python完全可以不依赖于GIL。
GIL(Global Interpreter Lock)译为全局解释器锁(这里的全局包括多个CPU下), 并不是Python独有特性.
GIL仅是Python官方解释器CPython引入的一个互斥锁(mutex)概念
(除它之外,还有PyPy,Psyco,JPython,IronPython等,在绝大多数情况下,我们通常都认为 Python == CPython,所以也就默许了Python具有GIL锁这个事)**。
所以,当每个线程在执行时都需要先获取GIL,保证同一时刻只有一个线程可以执行机器码。
因此,当使用Cpython解释器解释多线程程序时,为了保证线程安全,即共享资源的原子性。默认使用了一个GIL,导致多线程实际是“伪多线程”。

理解GIL的必要性,需要了解CPython对于线程安全的内存管理机制。

首先,我们来看看单核CPU下,多线程任务是如何调度的:
在这里插入图片描述
图可知,由于GIL的机制,单核CPU在同一时刻只有一个线程在运行。当线程遇到IO操作或Timer Tick到期,释放GIL锁。其他的两个线程去竞争这把锁,得到锁之后,才开始运行。

线程释放GIL锁有两种情况,一是遇到IO操作,二是Time Tick到期。IO操作很好理解,比如发出一个http请求,等待响应。那么Time Tick到期是什么呢?Time Tick规定了线程的最长执行时间,超过时间后自动释放GIL锁。

虽然都是释放GIL锁,但这两种情况是不一样的。比如,Thread1遇到IO操作释放GIL,由Thread2和Thread3来竞争这个GIL锁,Thread1不再参与这次竞争。如果是Thread1因为Time Tick到期释放GIL,那么三个线程可以同时竞争这把GIL锁,可能出现Thread1在竞争中胜出,再次执行的情况。单核CPU下,这种情况不算特别糟糕。因为只有1个CPU,所以CPU的利用率是很高的。

多核CPU下,由于GIL锁的全局特性,无法发挥多核的特性,GIL锁会使得多线程任务的效率大大降低:
在这里插入图片描述
Thread1在CPU1上运行,Thread2在CPU2上
运行。GIL是全局的,CPU2上的Thread2需要等待CPU1上的Thread1让出GIL锁,才有可能执行。如果在多次竞争中,Thread1都胜出,Thread2没有得到GIL锁,意味着CPU2一直是闲置的,无法发挥多核的优势。

为了避免同一线程霸占CPU,在python3.x中,线程会自动的调整自己的优先级,使得多线程任务执行效率更高。

既然GIL降低了多核的效率,那保留它的目的是什么呢?这就和线程执行的安全有关。

准确的说,GIL的线程安全是粗粒度的。也就是说,有GIL都不意味着线程安全。比如下面这个例子:

def add():
    global n
    for i in range(10**1000):
        n = n +1
def sub():
    global n
    for i in range(10**1000):
        n = n - 1
n = 0
import threading
a = threading.Thread(target=add,)
b = threading.Thread(target=sub,)
a.start()
b.start()
#join 用于阻塞主线程,避免过早打印n
a.join()
b.join()
print n

上面的程序对n做了同样数量的加法和减法,那么n理论上是0。但运行程序,打印n,发现它不是0。问题出在哪里呢,问题在于python的每行代码不是原子化的操作。比如n = n+1这步,不是一次性执行的。如果去查看python编译后的字节码执行过程,可以看到如下结果。

19 LOAD_GLOBAL              1 (n)
22 LOAD_CONST               3 (1)
25 BINARY_ADD          
26 STORE_GLOBAL             1 (n)

从过程可以看出,n = n +1 操作分成了四步完成。因此,n = n+1不是一个原子化操作。

1.加载全局变量n,2.加载常数1,3.进行二进制加法运算,4.将运算结果存入变量n。

根据前面的线程释放GIL锁原则,线程a执行这四步的过程中,有可能会让出GIL。如果这样,n=n+1的运算过程就被打乱了。最后的结果中,得到一个非零的n也就不足为奇。

这就是为什么我们说GIL是粗粒度的,它只保证了一定程度的安全。如果要做到线程的绝对安全,是不是所有的非IO操作,我们都需要自己再加一把锁呢?答案是否定的。在python中,有些操作是是原子级的,它本身就是一个字节码,GIL无法在执行过程中释放。对于这种原子级的方法操作,我们无需担心它的安全。比如sort方法,[1,4,2].sort(),翻译成字节码就是CALL METHOD 0。只有一行,无法再分,所以它是线程安全的。

总结
程序分为计算密集型程序和I/O密集型程序。
计算密集型程序的特点是没有延时和阻塞,整段机器码几乎都用于计算的功能。
I/O密集型程序的特点是存在延时或堵塞,程序的功能主要是实现收发数据。
对于IO密集型应用,多线程的应用和多进程应用区别不大。即便有GIL存在,由于IO操作会导致GIL释放,其他线程能够获得执行权限。由于多线程的通讯成本低于多进程,因此偏向使用多线程。

对于计算密集型应用,多线程处于绝对劣势,可以采用多进程或协程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
PythonGIL(全局解释器)是一种机制,它确保在任何给定时间只有一个线程在解释器执行字节码。这意味着在多线程的情况下,Python的多线程并不能真正实现并行执行,而只是并发执行。 GIL的存在是为了保护Python解释器内部数据结构的一致性,因为这些数据结构在多线程环境下可能会出现竞争条件。然而,这也导致了Python在处理计算密集型任务时的性能问题,因为只有一个线程可以执行字节码。 虽然GIL对于IO密集型任务并不是一个问题,因为线程在等待IO操作完成时会释放GIL,但对于计算密集型任务,GIL会成为性能瓶颈。 要解决GIL的限制,有几种方法可以尝试: 1. 使用多进程而不是多线程:Python的multiprocessing模块提供了一种在多个进程执行任务的方式,每个进程都有自己的解释器和GIL。这样可以实现真正的并行执行。你可以使用multiprocessing模块来将计算密集型任务分配给多个进程执行。 2. 使用其他解释器:除了CPython,还有其他的Python解释器,如Jython、IronPython和PyPy。这些解释器没有GIL的限制,因此可以实现真正的并行执行。但需要注意的是,这些解释器可能不支持所有的Python库和功能。 3. 使用C扩展:对于计算密集型任务,可以使用C扩展来绕过GIL。通过将计算部分的代码编写为C扩展,可以在不受GIL限制的情况下执行计算。 下面是一个使用多进程的示例代码,演示了如何绕过GIL实现并行执行: ```python from multiprocessing import Pool def calculate_square(n): return n * n if __name__ == '__main__': numbers = [1, 2, 3, 4, 5] pool = Pool() result = pool.map(calculate_square, numbers) print(result) ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值