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

1. GIL是什么?

首先来看看GIL究竟是什么。我们需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。有名的编译器例如GCC,INTEL C++,Visual C++等。Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL。

2.GIL为什么会存在?

GIL的问题其实是由于近十几年来应用程序和操作系统逐步从多任务单核心演进到多任务多核心导致的 , 在一个古老的单核CPU上调度多个线程任务,大家相互共享一个全局锁,谁在CPU执行,谁就占有这把锁,直到这个线程因为IO操作或者Timer Tick到期让出CPU,没有在执行的线程就安静的等待着这把锁(除了等待之外,他们应该也无事可做)。下面这个图演示了一个单核CPU的线程调度方式:

很明显,在一个现代多核心的处理器上,上面的模型就有很大优化空间了,原来只能等待的线程任务,现在可以在其它空闲的核心上调度并发执行。由于古老GIL机制,如果线程2需要在CPU 2 上执行,它需要先等待在CPU 1 上执行的线程1释放GIL(记住:GIL是全局的)。如果线程1是因为 i/o 阻塞让出的GIL,那么线程2必定拿到Gil。但如果线程1是因为timer ticks计数满100让出GIL,那么这个时候线程1和线程2公平竞争。但要命的是,在Python 2.x, 线程1不会动态的调整自身的优先级,所以很大概率下次被选中执行的还是线程1,在很多个这样的选举周期内,线程2只能安静的看着线程1拿着GIL在CPU 1上欢快的执行。

在稍微极端一点的情况下,比如线程1使用了while True在CPU 1 上执行,那就真是“一核有难,八核围观”了,如下图所示:

接下来,我们用实际代码来看看GIL的存在对多线程运行的影响

import threading, timedef my_counter():i = 0for _ in range(100000000):i = i + 1return Truedef main1():thread_ary = {}start_time = time.time()for tid in range(2):t = threading.Thread(target=my_counter)t.start()t.join()  # 第一次循环的时候join方法引起主线程阻塞,但第二个线程并没有启动,所以两个线程是顺序执行的print("单线程顺序执行total_time: {}".format(time.time() - start_time))def main2():thread_ary = {}start_time = time.time()for tid in range(2):t = threading.Thread(target=my_counter)t.start()thread_ary[tid] = tfor i in range(2):thread_ary[i].join()  # 两个线程均已启动,所以两个线程是并发的print("并发执行total_time: {}".format(time.time() - start_time))if __name__ == "__main__":main1()main2()

python3运行结果

Python2运行结果

GIL是否意味着线程安全

有GIL并不意味着python一定是线程安全的,那什么时候安全,什么时候不安全,我们必须搞清楚。之前我们已经说过,一个线程有两种情况下会释放全局解释器锁,一种情况是在该线程进入IO操作之前,会主动释放GIL,另一种情况是解释器不间断运行了1000字节码(Py2)或运行15毫秒(Py3)后,该线程也会放弃GIL。既然一个线程可能随时会失去GIL,那么这就一定会涉及到线程安全的问题。GIL虽然从设计的出发点就是考虑到线程安全,但这种线程安全是粗粒度的线程安全,即不需要程序员自己对线程进行加锁处理(同理,所谓细粒度就是指程序员需要自行加、解锁来保证线程安全,典型代表是 Java , 而 CPthon 中是粗粒度的锁,即语言层面本身维护着一个全局的锁机制,用来保证线程安全)。那么什么时候需要加锁,什么时候不需要加锁,这个需要具体情况具体分析。下面我们就来针对每种可能的情况进行分析和总结。

首先来看第一种线程释放GIL的情况。假设现在线程A因为进入IO操作而主动释放了GIL,那么在这种情况下,由于线程A的IO操作等待时间不确定,那么等待的线程B一定会得到GIL锁,这种比较“礼貌的”情况我们一般称为“协同式多任务处理”,相当于大家按照协商好的规则来,线程是安全的,不需要额外加锁。

接下来,我们来看另外一种情况,即线程A是因为解释器不间断执行了1000字节码的指令或不间断运行了15毫秒而放弃了GIL,那么此时实际上线程A和线程B将同时竞争GIL锁。在同时竞争的情况下,实际上谁会竞争成功是不确定的一个结果,所以一般被称为“抢占式多任务处理”,这种情况下当然就看谁抢得厉害了。当然,在python3上由于对GIL做了优化,并且会动态调整线程的优先级,所以线程B的优先级会比较高,但仍然无法肯定线程B就一定会拿到GIL。那么在这种情况下,线程可能就会出现不安全的状态。针对这种纯计算的操作,我们用一段代码来演示下这种线程不安全的状态。代码如下:

import threadingn = 0def add():
global n
for i in range(1000000):
n = n + 1def sub():
global n
for i in range(1000000):
n = n - 1if __name__ == "__main__":
t1 = threading.Thread(target=add)
t2 = threading.Thread(target=sub)t1.start()
t2.start()t1.join()
t2.join()print("n的值为:", n)

上面的代码很简单,分别用线程1和线程2对全局变量n进行了1000000次的加和减操作。如果线程安全的话,那么最终的结果n应该还是为0。但实际上,我们运行之后,会发现这个n的值有时大有时小,完全不确定。这就是典型的多个线程操作同一个全局变量造成的线程不安全的问题。我们明白了这个问题,在这里我们只讨论产生问题的原因,在后面的文章中再来讨论如何通过加锁来解决这个问题。

接下来,我们从代码层面分析下产生这个问题的原因。在线程中,我们主要是执行了一个加法和减法的操作。为了方便说明问题,我们把函数最简化到一个加法函数和一个减法函数,来分析它们的字节码执行过程,来看看释放GIL锁是怎么引起这个问题的。演示代码如下:

import dis
n = 0def add():global nn = n + 1print(dis.dis(add))def sub():global nn = n - 1
print(dis.dis(sub))

dis模块中的dis方法可以打印出一个函数对应的字节码执行过程,所以非常方便我们进行分析。运行结果如下:

不管是加法还是减法运算,都会分为4步完成。以加法为例,第一步是LOAD_GLOBAL(加载全局变量n),第二步LOAD_CONST(加载常量1),第三步进行二进制的加法,第四步将计算结果存储到全局变量n中,加法计算结束。这四个指令如果能够保证被作为一个整体完整地运行,那么是不会产生问题的,但根据前面说的线程释放GIL的原则,那么很有可能在线程正在执行这四步中的任何一步的时候释放掉GIL而进入等待状态,这个时候发生的事情就比较有意思了。为了方便大家理解,我拿一种比较极端的情况来说明一下。比如我们在加法运算中,正准备执行第四步的时候,很不幸失去了GIL,进入等待状态(注意此时n值仍然为0)。减法运算的线程开始执行,它加载了全局变量n(值为0),并进行减法相关的计算,它也在执行第三步的时候失去了GIL,此时它进入等待状态,加法运算继续。上一次加法计算继续运行第4步,即把加法运算结果赋值给全局变量n,那么此时n的值为1。同样道理,减法操作拿回GIL时,它之前已经加载了为0的n的值,所以它继续操作到最后赋值那步时,n的值就为0-1=-1。换句话说,n的值要么为1,要么为-1,但我们期望的应该是0。这就造成了线程不安全的情形。最终,经过百万次这样不确定的加减操作,那么结果一定是不确定的。这就是引起这个问题的过程和原因。

综上所述我们便能很好的理解为什么经常说Python的多线程实际上并不是真正意义上的多线程,因为多线程的速度还不如单线程运行的速度。那么我们什么时候使用多线程以及多进程呢?

只要在进行耗时的IO操作的时候,能释放GIL,所以只要在IO密集型的代码里,用多线程就很合适

用于计算密集型,比如上面我们的例子,进行大型的计算的时候,CPU一直在执行运算的时候就只适合进程了

https://www.cnblogs.com/TMesh/p/11731452.html

python中对GIL的理解相关推荐

  1. 深入理解Python中的GIL(全局解释器锁)。

    Python是门古老的语言,要想了解这门语言的多线程和多进程以及协程,以及明白什么时候应该用多线程,什么时候应该使用多进程或协程,我们不得不谈到的一个东西是Python中的GIL(全局解释器锁).这篇 ...

  2. 深入理解Python中的GIL(全局解释器锁)

    前言:本博文主要讲解Python中的GIL(全局解释器锁),本人经过查阅多个资料,摒弃一些较官方的描述,用自己的语言整理并加以补充,如果有描述有误的地方,还望读者在评论区指出,谢谢! 文章目录 一.G ...

  3. Python中的GIL和深浅拷贝

    Python中的GIL和深浅拷贝 文章目录 Python中的GIL和深浅拷贝 一.GIL全局解释器锁 1.引入 2.GIL 3.Python GIL底层实现原理 4.计算密集型和IO密集型 5.解决G ...

  4. python中的GIL详解

    python中的GIL详解 参考Python-- GIL 锁简述 GIL是什么 首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念.就 ...

  5. python self 值自动改变,在python中对self的理解

    在python中对self的理解 : 一.self的位置是出现在哪里? 首先,self是在类的方法中的,在调用此方法时,不用给self赋值,Python会自动给他赋值,而且这个值就是类的实例--对象本 ...

  6. Python中的GIL锁

    Python中的GIL锁 在Python中,可以通过多进程.多线程和多协程来实现多任务. 在多线程的实现过程中,为了避免出现资源竞争问题,可以使用互斥锁来使线程同步(按顺序)执行. 但是,其实Pyth ...

  7. Python中if __name__=='__main__': 理解与总结(看这篇就够了,一文扫清疑惑!)

    前言 在Python当中,如果代码写得规范一些,通常会写上一句if '__name__'=='__main__:'作为程序的入口,但似乎没有这么一句代码,程序也能正常运行.这句代码多余吗?原理又在哪里 ...

  8. Python中timedelta类型的理解

    Python中timedelta类型的理解 逻辑: timedelta = datetime1-datetime2 理解:一个时间等于两个时刻做差 代码 import datetimeif __nam ...

  9. Python中的GIL和异步Asyncio、Futures

    一 .基本概念 以下概念都是在 Python 环境下 Sync 同步编程 Async 异步 ,是指在外观上看来程序不会等待,而是找出可执行的操作/任务/线程 继续执行 Asyncio 单个主线程 多个 ...

最新文章

  1. mfc---手动给toolbar按钮添加消息View中
  2. react中高阶组件
  3. Java学习_day004:Scanner与分支结构
  4. 甘肃宕昌山货“触网”外销:山民乐衷创业“等客来”
  5. HDU-2444 The Accomodation of Students
  6. PL/SQL Developer调试Oracle存储过程
  7. Flutter AnimatedWidget 实现动画的自动刷新
  8. eBPF学习记录(四)使用libbpf开发eBPF程序
  9. [Android疑难杂症]动态设置TextView的width不起作用
  10. C++程序设计-第十周循环结构程序设计上机实践项目
  11. 去掉讨厌的“windows盗版软件受害者”的提示
  12. 产品需求文档怎样编写
  13. 21年寒假第二周周练 蒜厂年会(一)最大连续子序列和
  14. miRNA 在基因调控中的作用
  15. 浅谈屏幕适配 dp dip sp dpi ppi px sp
  16. [terry笔记]Python字符串
  17. vue 使用gsap(TweenMax)
  18. 亿道丨三防平板丨加固平板丨三防加固平板丨改善资产管理
  19. 基于java的springboot多用户商城(类淘宝京东)系统毕业设计springboot开题报告
  20. 大端和小端C++转载记录

热门文章

  1. 大一新生计算机专业要选课,大一新生开学需要带电脑吗,辅导员:看看这四点再做决定...
  2. pixy php,Pixy2与STM32进行SPI通信
  3. java epson指令集_EPSON机械手 SPEL+语言指令集
  4. 计算机研究生就业方向之当老师(中小学)
  5. Android 手势锁/锁屏/Pin解锁,一种精简高大上且实用的手势锁
  6. Computational and Mathematical Methods in Medicine
  7. HDU 4489 The King's Ups and Downs
  8. Android 的触摸反馈以及事件分发机制
  9. google service
  10. oppoR9m降级 root刷机 Magiskroot 解锁system文件夹