暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Python线程前置知识点-GIL全局解析器

小儿来一壶枸杞酒泡茶 2021-05-11
801

Python线程前置知识点-GIL全局解析器

关于python多线程处理并发的话,这里就不得不再提一次GIL,之前也有提过关于这个GIL的一下简单介绍,这里也为了完整性,这里也复述一下。

因为GIL的存在,python中的多线程,是伪线程。

我们的知道我们的线程执行的是可以如果是在多核的的情况下,是可以分配到多核上进行执行的。如:java的多线程如果是在多核的情况下是可以分别在多核上进行执行的。

但是python的因为历史原因,久久未能把这个历史遗留的问题给处理,估计后续也不会处理了!

但是对于GIL的认知,需要梳理清楚几个问题:

1:什么是GIL?

说简单点就是,因为我们的python内存管理机制采用了引用计数的方式来处理垃圾回收,为了相关统计引用,以及方便进行数据管理,相关的Cpython解析器,规定:某个线程的执行,必须先要拿到GIL(全局解释锁)。

当我们的一个python程序跑起来之后,就会形成一个python程序进程,这个进程纸只有且只有一个GIL,在这个进程中的线程,如果需要执行,需要获取先拿到GIL 全局解释器锁。

如,一个四线程的程序,跑在一个四核的CPU上的话:

因为python调用操作系统的底层C语音的原生接口,就只是C的原生线程,在都多线程的情况在修改数据的时候,为了防止数据改乱了,在出口做了设置,把多线程执行变为串行处理,也就是所谓的全局解释器锁,保证了数据统一和安全。

PS:GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念,是相关python解析器所有。

所以,解释器加锁以后:

线程的执行其实是串行,或(微观上的并发执行),GIL上锁后意味着同一时间只有一个CPU在处理你的线程。

2.为什么需要引入GIL:

官方的说法是:

  • 在cpython中,gil是一个互斥锁,用来保护对python对象的访问,防止多个线程同时执行python字节码。

  • 在CPython的线程是操作系统的原生线程,在Linux的pthread完全由操作系统调度执行。pthread本身不是线程安全的,所以如果需要进行保证数据一致性,需要使用者通过锁来实现保证多线程的安全运行,因此CPython解释器下的Python实现多线程也必然存在线程不安全的问题。

所以,多线程下执行python字节码的情况下,所以加GIL这个锁(然而,由于GIL的存在,其他新增的特性,为了省事都在默认线程安全的前提下进行,导致GIL成了不可或缺的依赖。)

所以从官网说辞可以知道是引入的主要原因历史遗留原因:

  • 1:首先并发变成的需求是,随着CPU发展而来的。所以我们的多线程编程也是是随着多核CPU发展而发展,但早起我们的cpython都是基于都是在单核CPU的前提下设计的,没考虑过多核的情况。

  • 2:cpython在并发的需求情况下,为了利用多核,Python也开始支持多线程,但是它的内存管理不是线程安全的,为了解决多线程之间数据完整性和状态同步的,最简单方法自然就是加锁,为了更好进行内存更好的进行管理所以只能加GIL这个全局锁。

3.Python解释器有哪些?

  • Jython 一种由java语言编写的python解释器,是将python编译成Java字节码然后执行的一种解释器,可以方便地和Java的类库进行交互。

  • IronPython 将Python代码解释为.Net平台上运行的字节码进行执行,类似Jython解释器,可以方便的和.Net平台上的类库进行交互。

  • IPython

在交互效果上有所增强,但执行过程和功能方面和CPython是一样的。

  • PyPy

一种使用JIT(just-in-time)技术的编译器,专注于执行速度,对Python代码进行动态编译,从而提高Python的执行速度。

PS:PyPy在处理python代码的过程中,一小部分功能的处理和CPython的执行结果是有差异的,如果用PyPy建议先了解下PyPy和CPython的区别。

  • CPython

CPython是主流版本的解释器,这个解释器是使用C语言编写的,也是使用最为广泛的解释器,可以方便地和C/C++的类库进行交互。

4.CPython解释器执行代码过程是怎么样流程?

Python是一门动态语言,它是边解释边执行的

  • 程序员写的源代码
  • 在运行时需先被转换成字节码.cpy
  • 然后再转换为机器码
  • CPU执行机器码。

整个过程就是:执行过程中:源代码-->字节码-->机器码,整个过程由是Python解释器处理完成的。

早期的GIL的执行机制是:

  • 计数器+I/O操作阻塞+部分指令比较简单的不用计数
    • 常规情况下数到100就释放锁,让其他线程有机会执行
    • 另外如果遇到阻塞情况,也要提前释放锁让其他线程有机会执行
    • 部分的指令不累加到计数里面

后期的GIL的执行机制是:

原因是:

  • 多核情况下多线程执行时,计数器模式存在缺陷,当一个线程在CPU-1执行完之后释放GIL之后,存在一定的概率,又被自己把锁给抢夺回来

  • 而此时其他CPU线程可能被唤醒后,进行竞争的申请锁,发现锁还在别的线程那,浪费了,并且有可能会一直得不到锁的情况。

  • 多核CPU频繁进行线程切换.

  • 按执行照字节码指令数来统计,有些指令码简单执行快,,有些很复杂,导致计数也存在缺陷

升级后:

  • 去除计数的方式,改为使用时间片的方式(每个线程的执行时间片是5000微秒)左右。
  • 新增一个锁实现强制线程切换,避免保证释放GIL后,又被自己马上又抢到。

5:那是不是我们的多线程就没任何意义了?

通常我们对于任务的执行分:IO密集型和CPU计算密集型。

PS:CPU性能的具体作用的体现:

  • 在I/O密集型的程序中,CPU性能好坏无法决定执行的速度。
  • 在CPU密集型的程序中,CPU性能好坏决定执行的速度,算力速度的比较!

而对于任务作业的这些我们的执行程序执行通常有两种状态:

  • 阻塞:遇到IO,CPU需等待IO完成
  • 非阻塞:遇到IO,CPU切换,不需要等待IO完成,后续再进行根据情况切换回来。

6:GIL锁和线程锁执行流程图示

通常我们的python中的线程如果遇到的IO的时候,会自动的进行GIL锁就的释放,让给其他的线程执行。

7:GIL锁和线程互斥锁的区别:

  • GIL不能保证自己开启的线程安全,但能保证解释器中的数据是线程安全。(嫌贵与解锁器对象而言)

  • GIL 在线程调用解释器时,会进行自动加锁,而在线程遇到IO阻塞时或线程代码执行完毕时会进行自动解锁。

  • 线程互斥锁- 是相对于程序内部的相关的数据对象而言,锁的是程序内部操作的资源对象。

  • GIL锁的python字节码执行码

8:如何解决GIL?

  • 如果无并发需求,忽略即可
  • 分析需求,如相关的IO密集型任务,使用多线程也是可以提升到一定效率滴。
  • 再不行就搬出来multiprocess(多进程)替代Thread可供选择。

multiprocess库的出现很大程度上是为了弥补thread库因为GIL而低效的缺陷。

  • 它完整的复制了一套thread所提供的接口方便迁移。
  • 每个进程有自己的独立的GIL,因此也不会出现进程之间的GIL争抢,通过多进程的方式绕过GIL。
  • 再不行使用协程(用户态线程)-(单进程单线程模式)
  • 通过编写C语言扩展与Python交互,在C语言层面绕过GIL实现多核利用。

个人其他博客地址

简书:https://www.jianshu.com/u/d6960089b087

掘金:https://juejin.cn/user/2963939079225608

小钟同学 | 文 【原创】| QQ:308711822

  • 1:本文相关描述主要是个人的认知和见解,如有不当之处,还望各位大佬指正。
  • 2:关于文章内容,有部分内容参考自互联网整理,如有链接会声明标注;如没有及时标注备注的链接的,如有侵权请联系,我会立即删除处理哟。


文章转载自小儿来一壶枸杞酒泡茶,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论