谈谈Python全局解释器锁GIL

https://blog.csdn.net/weixin_41931540/article/details/87937046
https://www.jianshu.com/p/de0b71cada92

GIL的全称是Global Interpreter Lock(全局解释器锁),

1.1,为何要引入GIL?

首先,看看多核cpu的元年和python的诞生日期:
多核cpu的发展历程

但真正的“双核元年”,则被认为是2006年

python语言诞生于哪一年

1991年,第一个Python编译器(同时也是解释器)诞生。
它是用C语言实现的,并能够调用C库(.so文件)。从一出生,Python已经具有了:
类(class),函数(function),异常处理(exception),包括表(list)和词典(dictionary)在内的核心数据类型,以及模块(module)为基础的拓展系统。

其次,我们看看python之父怎么说的:

在python之父的一些谈话中,可以看出一些些猫腻。
有些听众问到global interpreter lock(GIL),想要更深入了解这个问题,以及这个问题是如何解决的。Van
Rossum笑着反问道:“你有多少时间?”他简要的讲述了GIL产生的历史。在Python诞生后,多核计算机出现了。当线程运行在不同的内核中时,两个或更多的处理器要更新同一个对象便产生了竞争机制,特别在Python垃圾回收处理机制中。
一个合理的解决方案就是给每个对象上锁,这样能保护数据不被多路存取破坏。但结果导致当没有锁的竞争时,上锁和解锁操作代价高昂。一些实验表明,不需要上锁的单线程程序性能会因此降低2倍。这意味着只有在使用三个或多个线程或内核的程序会从中获益。
因此,GIL诞生了(尽管这个名字在它被添加到解释器后很久才出现)。它是一个立刻有效锁定所有对象的单一锁,这样所有对象访问将排序进行。目前的问题是,10年或15年以后,多核处理器无处不在,人们想要不必进行多重处理就可利用它们(例如,使用独立的进程而不是线程通信)
他说,如果你当今想要设计一种新语言,要让它没有易变的对象,或者有限的易变性。然而,听众中传来“这就不是 Python 了”。VanRossum 赞成的说:“你说出了我要说的话”。GIL 周围有很多开发者不断的努力,包括 PyPy 软件事务内存(STM),以及
PyParallel。其他开发者也撞破了脑袋在想解决办法。如果有人知道有什么办法能够移除 GIL 且让语言保持 Python 特性,Van和其他人将很乐意听到。

多核可能会导致多线程(补充一下:进程是向系统申请资源的最小单位,而线程是CPU调度的最小单位,一个进程至少拥有一个线程)之间可能同一时刻竞争同一对象而导致意外的发生,干脆给python解释器上了一把大锁,即同一时刻一个python解释器里只能运行一个线程,众所周知python最先流行于科学家手中,是进行科学计算的利器,在科学家眼中,浪费CPU总比耗费人力重写已然庞大的历史库要强,所以为了仍然兼容以前的库,就引入了线程全局解释器锁,以至于后来新发展的库也仍然基于这个特性,可谓一发不可收拾,成了无法根除的历史遗留问题。注意GIL只是Cpython中的特性,其它基于java的Jpython等解释器就没有这个问题。

补充:Python解释器的种类

CPython 当我们从Python官方网站下载并安装好Python 3.5后,我们就直接获得了一个官方版本的解释器:CPython。这个解释器是用C语言开发的,所以叫CPython。在命令行下运行python就是启动CPython解释器。
CPython是使用最广的Python解释器

IPython IPython是基于CPython之上的一个交互式解释器,也就是说,IPython只是在交互方式上有所增强,但是执行Python代码的功能和CPython是完全一样的。好比很多国产浏览器虽然外观不同,但内核其实都是调用了IE。

CPython用>>>作为提示符,而IPython用In [序号]:作为提示符。

PyPy PyPy是另一个Python解释器,它的目标是执行速度。PyPy采用JIT技术,对Python代码进行动态编译(注意不是解释),所以可以显著提高Python代码的执行速度。
绝大部分Python代码都可以在PyPy下运行,但是PyPy和CPython有一些是不同的,这就导致相同的Python代码在两种解释器下执行可能会有不同的结果。如果你的代码要放到PyPy下执行,就需要了解PyPy和CPython的不同点。

Jython Jython是运行在Java平台上的Python解释器,可以直接把Python代码编译成Java字节码执行。

IronPython IronPython和Jython类似,只不过IronPython是运行在微软.Net平台上的Python解释器,可以直接把Python代码编译成.Net的字节码。

1.2,GIL导致的问题

由于GIL的限制,Cpython解释器在任一时刻只允许一个Python线程投入执行。GIL带来的最明显的影响就是多线程的python程序无法充分利用多个CPU核心带来的优势(即,一个采用多线程技术的计算密集型应用某一时刻只能在多核CPU的一个CPU上运行)

1.3,代码验证多进程多线程的实际运行情况

示例:
如果Ubuntu设置的是四核,也就是说同时可以执行两个任务,那么下面可以进行试验:

试验1:运行如下代码

while True:
    pass

这是一个死循环,在终端输入htop
在这里插入图片描述
发现原本是四核的Ubuntu,有一核被充满了,这就说明执行上述代码的时候单线程已经占用了一个核了,当终止运行的时候,会发现又回到了刚开始的状态。(注意图中显示统计的是一段时间内的CPU平均使用量而不是某一时刻的CPU状态,否则我们将看到同一时刻将只有一个CPU被使用)

试验2:运行下面代码

from threading import Thread
 
def test():
    while True:
        pass
 
def main():
    th = Thread(target=test)
    th.start()
    while True:
        pass
 
if __name__=='__main__':
    main()

运行结果如下:
在这里插入图片描述
可以看到四个核的总占用大约就是100%,也就是说这个代码虽然是双线程的但是却不是真正的并发,就相当于有个互斥锁的存在,本质上来看还是单线程

试验3:运行下面代码

from multiprocessing import Process
 
def test():
    while True:
        pass
 
def main():
    th = Process(target=test)
    th.start()
    while True:
        pass
 
if __name__=='__main__':
    main()

在这里插入图片描述
可以看到这个运行结果是一个核先占满,这就说明在进程中运行的时候才是真的并发运行,也就是是真的有多进程在运行,这是因为进程中没有GIL锁,而线程中有GIL锁

因为我们一般用的都是C写的Python解释器,这样的话就会有GIL锁的问题;之前所说的进程比线程占用更多的资源,但是进程可以实现真正的多任务,所以二者各有优缺点。如果是使用Java写的解释器那么就没有GIL锁的问题,当然选择线程会更好

线程虽然会有GIL锁的问题,但是一般来说多线程实现多任务还是比单线程更快的,因为每个进程都或多或少地存在阻塞,为何这样说呢,参考如下:
阻塞
一个函数在返回前等待某件事情发生时称为阻塞。一个函数可能由于许多原因而阻塞:网络I/O、磁盘I/O、互斥锁等。 事实上,每一个 函数当它运行和使用CPU时就至少产生了一点点阻塞(举一个极端的例子,它演示了为什么必须像其他类型的阻塞那样重视CPU阻塞,考虑密码散列函数如 bcrypt, 它设计使用数百毫秒的CPU时间,远远超过一次典型的网络或磁盘访问时间)

1.4,python多进程和多线程的适用场景

计算密集型编程:代码中充斥着大量的计算,没有延时或者recv接收等待函数,这时候可以通过多进程加快计算过程。
I/O密集型编程:即含有输入输出操作的编程,比如硬盘的读写速度相较于CPU的运算速度是非常慢的,也就是在运算过程中需要等待,这时候就可以用多线程来进行操作,某一线程运行过程中等待接收数据的同时其他线程可以进行输出或者计算,这样就可以节省时间
另外协程也是I/O密集型编程最佳候选之一。

1.5,如何规避GIL问题?

1.使用其它语言,例如C,Java
2.使用Cpython之外的其它python解释器,如java的解释器Jython等
3.使用多进程,协程等其它技术手段

1.6,Python中有GIL为什么还需要线程同步?

简单说:线程的锁机制与CPU的锁(GIL)粒度不一样,GIL并不能保证线程安全

单个字节码操作(原子操作)是线程安全的,非原子操作是多字节码操作
所以我理解的GIL只是使一些简单的原子操作变成了线程安全的,而非那些原子操作的过程仍然
需要我们自己使用锁机制进行同步
GIL保证的是字节码同一时刻只能被一个线程执行,但是同一个操作可能对应多个字节码的,比如:

In [1]: import dis
 
In [2]: dis.dis(lambda x: x+1)
  1           0 LOAD_FAST                0 (x)
              3 LOAD_CONST               1 (1)
              6 BINARY_ADD
              7 RETURN_VALUE

假设刚好执行到了RETURN_VALUE时切换到另一个线程执行了相同操作中,X的值就改变了。

1.7,GIL面试题参考答案:

GIL面试题如下:
描述Python GIL的概念, 以及它对python多线程的影响?编写一个多线程抓取网页的程序,并阐明多线程抓取程序是否可比单线程性能有提升,并解释原因。
参考答案:
Python语言和GIL没有什么关系。仅仅是由于历史原因在Cpython虚拟机(解释器),难以移除GIL。
GIL:全局解释器锁。每个线程在执行的过程都需要先获取GIL,保证同一时刻只有一个线程可以执行代码。
线程释放GIL锁的情况: 在IO操作等可能会引起阻塞的system call之前,可以暂时释放GIL,但在执行完毕后,必须重新获取GIL Python 3.x使用计时器(执行时间达到阈值后,当前线程释放GIL)或Python 2.x,tickets计数达到100。
Python使用多进程是可以利用多核的CPU资源的。
多线程爬取比单线程性能有提升,因为遇到IO阻塞会自动释放GIL锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值