飞道的博客

浅谈Python GIL全局解释器锁

270人阅读  评论(0)

前言:GIL的知识大多都是在面试的时候才会用到,但是抱着学习(或者说更好的与面试官扯皮)的心态,怎么也要了解的深入一些

正文:

GIL全局解释器锁

前提:GIL全局解释器锁是CPython解释器独有的锁,目的就是牺牲效率保证数据安全。
一直有CPython的并发不太行的说法,是真的呢?还是一些大V危言耸听?吸引眼球还是确有其事?接下来就跟你详细剖析一些GIL全局解释器锁

通过命令执行python文件的流程

操作系统在内存中开辟一个进程空间,将你的Python解析器以及py文件加载进去,解释器运行py文件。我们可以把Python解释器看做一个函数,你的py代码就是一堆代码,相当于一个实参,然后将这个实参传入函数中(Python解释器)执行。此时我们所说的Python解释器就是CPython解释器。

你的Python解释器细分为三部分

  • 先将你的代码通过编译器编译成C的字节码
  • 然后给到虚拟机将字节码输出为机器码
  • 最后配合操作系统把你的这个机器码扔给CPU去执行

    你的py文件中有一个主线程(红色箭头代表的就是主线程),主线程配合操作系统执行了整个过程。我们知道一个进程可以开启多个线程执行任务,那就意味着:理论上来说,一个进程的多个线程可以利用多核并行(不是并发)的处理任务。三个线程给Python三个CPU去并行的执行,最大限度提高效率。

    but! 这个只是理论上来说,实际上Python的单个进程的多线程是无法应用多核的,因为写Cpython源码的大佬程序员给进入解释器的线程加了一把锁,也就是我们常说的GIL锁。

为什么CPython解析器要加这把锁呢?

  • 因为在Python刚刚研发出来的时候是单核的时代,而且CPU价格非常的昂贵,Python起初作为一种脚本语言,面临的需求单核解决足以
  • 如果不加这把GIL锁,那么同一时刻进入CPython解释器线程数量不固定,我们要保证CPython解释器的数据资源安全,就需要在源码内部主动加入大量的互斥锁(Lock)来保证数据安全性,这样非常麻烦并且会降低CPython源码的开发速度

那现在为什么不将这把锁去掉呢?
CPython解释器内部的管理以及业务逻辑全部是围绕单线程实现的,并且从⻳叔创建CPython到现在,CPython源码已经更新迭代⻢上到4版本了,源码内容体量庞⼤,如果你要去掉,这个⼯程量⽆异于重新构建python,是⽐登天还难。

  • Cpython解释器是官⽅推荐的解释器,处理速度快,功能强⼤
  • JPython就是编译成Java识别的字节码,没有GIL锁
  • Pypy属于动态编译型,规则和漏洞很多,现在还在测试阶段(未来可能会成为主流)没有GIL锁

只有Cpython解释器有GIL锁,其他类型的解释器以及其他语⾔都没有

由此可以看出,CPython并发不太行是确有其事的,但是也不能因此否定Python,虽然说Python的单个进程的多线程不能利用多核并行处理脚本,但是并不是说Python不能并行,是单个进程下的多线程不能并行,如果你要开启多个进程是可以利用多核的。那么继续发问了,多个进程不是开销⾮常⼤么?这不是影响性能么?其实这个只是相对的,⼗⼏个进程对于我们现在⽤的
平常的电脑是不成问题的,何况企业级服务器呢?

其实,即使说单个进程的多个线程不能并行。也不会影响很多的。不能并行那就并发吧,这就要针对计算密集型还是IO密集型分情况讨论了

IO密集型:

操作系统可以操控着CPU遇到IO就将CPU强⾏的切执⾏另⼀个任务,⽽这个任务遇到IO阻塞了,⻢上⼜会切换,所以IO密集型利⽤单个进程的多线程并发是最好的解决⽅式(后⾯还会有协程也⾮常好⽤)。

计算密集型:

多个任务都是纯计算都没有IO阻塞,那么此时应该利⽤多进程并⾏的处理任务。

总结

GIL全局解释器锁只存在Cpython解释器中,他是给进⼊解释器的线程上锁:

  • 优点:便于Cpython解释器的内部资源管理,保证了Cpython解释器的数据安全。
  • 缺点:单个进程的多线程不能利⽤多核。
  • 注意:GIL全局解释器锁并不是让Cpython不能利⽤多核,多进程是可以利⽤多核的,况且IO密集型
    的任务,单个进程的多线程并发处理⾜以。

IO密集型:单个进程的多线程并发处理
计算密集型:多个进程并⾏处理

.GIL全局解释器锁与互斥锁的关系(补充)

Python已经有⼀个GIL来保证同⼀时间只能有⼀个线程来执⾏了,为什么这⾥还需要Lock?

⾸先我们需要达成共识:锁的⽬的是为了保护共享的数据,同⼀时间只能有⼀个线程来修改共享的数据
然后,我们可以得出结论:
保护不同的数据就应该加不同的锁。

最后,问题就很明朗了,GIL 与Lock是两把锁,保护的数据不⼀样,前者是解释器级别的(当然保护
的就是解释器级别的数据,⽐如垃圾回收的数据),后者是保护⽤户⾃⼰开发的应⽤程序的数据,很明
显GIL不负责这件事,只能⽤户⾃定义加锁处理,即Lock。

过程分析:所有线程抢的是GIL锁,或者说所有线程抢的是执行权限

线程1抢到GIL锁,拿到执行权限,开始执行,然后加了一把Lock,还没有执行完毕,即线程1还未释放Lock,有可能线程2抢到GIL锁,开始执行,执行过程中发现Lock还没有被线程1释放,于是线程2进入阻塞,被夺走执行权限,有可能线程1拿到GIL,然后正常执行到释放Lock。。。这就导致了串行运行的效果

  既然是串行,那我们执行

  t1.start()

  t1.join

  t2.start()

  t2.join()

  这也是串行执行啊,为何还要加Lock呢,需知join是等待t1所有的代码执行完,相当于锁住了t1的所有代码,而Lock只是锁住一部分操作共享数据的代码。!!!

join是等待所有的代码执行完之后,而加锁只是锁上了一部分操作共享数据的代码,更加灵活,看不同的场景用来应用join还是lock

因为Python解释器帮你自动定期进行内存回收,你可以理解为python解释器里有一个独立的线程,每过一段时间它起wake up做一次全局轮询看看哪些内存数据是可以被清空的,此时你自己的程序 里的线程和py解释器自己的线程是并发运行的,假设你的线程删除了一个变量,py解释器的垃圾回收线程在清空这个变量的过程中的clearing时刻,可能一个其它线程正好又重新给这个还没来及得清空的内存空间赋值了,结果就有可能新赋值的数据被删除了,为了解决类似的问题,python解释器简单粗暴的加了锁,即当一个线程运行时,其它人都不能动,这样就解决了上述的问题, 这可以说是Python早期版本的遗留问题。


转载:https://blog.csdn.net/qq_39915318/article/details/106728853
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场