你是否真的了解全局解析锁(GIL)
关于我
一个有思想的程序猿,终身学习实践者,目前在一个创业团队任team lead,技术栈涉及Android、Python、Java和Go,这个也是我们团队的主要技术栈。
Github:https://github.com/hylinux1024
微信公众号:终身开发者(angrycode)
0x00 什么是全局解析锁(GIL)
A global interpreter lock (GIL) is a mechanism used in computer-language interpreters to synchronize the execution of threads so that only one native thread can execute at a time. --引用自wikipedia
从上面的定义可以看出,GIL
是计算机语言解析器用于同步线程执行的一种同步锁机制。很多编程语言都有GIL
,例如Python
、Ruby
。
0x01 为什么会有GIL
Python
作为一种面向对象的动态类型编程语言,开发者编写的代码是通过解析器顺序解析执行的。
大多数人目前使用的Python
解析器是CPython
提供的,而CPython
的解析器是使用引用计数来进行内存管理,为了对多线程安全的支持,引用了global intepreter lock
,只有获取到GIL
的线程才能执行。如果没有这个锁,在多线程编码中即使是简单的操作也会引起共享变量被多个线程同时修改的问题。例如有两个线程同时对同一个对象进行引用时,这两个线程都会将变量的引用计数从0增加为1,明显这是不正确的。
可以通过sys
模块获取一个变量的引用计数
>>> import sys
>>> a = []
>>> sys.getrefcount(a)
2
>>> b = a
>>> sys.getrefcount(a)
3
sys.getrefcount()
方法中的参数对a的引用也会引起计数的增加。
是否可以对每个变量都分别使用锁来同步呢?
如果有多个锁的话,线程同步时就容易出现死锁,而且编程的复杂度也会上升。当全局只有一个锁时,所有线程都在竞争一把锁,就不会出现相互等待对方锁的情况,编码的实现也更简单。此外只有一把锁时对单线程的影响其实并不是很大。
0x02 可以移除GIL吗?
Python
核心开发团队以及Python
社区的技术专家对移除GIL
也做过多次尝试,然而最后都没有令各方满意的方案。
内存管理技术除了引用计数外,一些编程语言为了避免引用全局解析锁,内存管理就使用垃圾回收机制。
当然这也意味着这些使用垃圾回收机制的语言就必须提升其它方面的性能(例如JIT
编译),来弥补单线程程序的执行性能的损失。
对于Python
的来说,选择了引用计数作为内存管理。一方面保证了单线程程序执行的性能,另一方面GIL
使得编码也更容易实现。
在Python
中很多特性是通过C
库来实现的,而在C
库中要保证线程安全的话也是依赖于GIL
。
所以当有人成功移除了GIL
之后,Python
的程序并没有变得更快,因为大多数人使用的都是单线程场景。
0x03 对多线程程序的影响
首先来GIL
对IO
密集型程序和CPU
密集型程序的的区别。
像文件读写、网络请求、数据库访问等操作都是IO
密集型的,它们的特点需要等待IO
操作的时间,然后才进行下一步操作;而像数学计算、图片处理、矩阵运算等操作则是CPU
密集型的,它们的特点是需要大量CPU
算力来支持。
对于IO
密集型操作,当前拥有锁的线程会先释放锁,然后执行IO
操作,最后再获取锁。线程在释放锁时会把当前线程状态存在一个全局变量PThreadState
的数据结构中,当线程获取到锁之后恢复之前的线程状态
用文字描述执行流程
保存当前线程的状态到一个全局变量中
释放GIL
... 执行IO操作 ...
获取GIL
从全局变量中恢复之前的线程状态
下面这段代码是测试单线程执行500万次消耗的时间
import time
COUNT = 50000000
def countdown(n):
while n > 0:
n -= 1
start = time.time()
countdown(COUNT)
end = time.time()
print('Time taken in seconds -', end - start)
# 执行结果
# Time taken in seconds - 2.44541597366333
在我的8核的macbook
上跑大约是2.4秒,然后再看一个多线程版本
import time
from threading import Thread
COUNT = 50000000
def countdown(n):
while n > 0:
n -= 1
t1 = Thread(target=countdown, args=(COUNT // 2,))
t2 = Thread(target=countdown, args=(COUNT // 2,))
start = time.time()
t1.start()
t2.start()
t1.join()
t2.join()
end = time.time()
print('Time taken in seconds -', end - start)
# 执行结果
# Time taken in seconds - 2.4634649753570557
上文代码每个线程都执行250万次,如果线程是并发的,执行时间应该是上面单线程版本的一半时间左右,然而在我电脑中执行时间大约为2.5秒!
多线程不但没有更高效率,反而还更耗时了。这个例子就说明Python
中的线程是顺序执行的,只有获取到锁的线程可以获取解析器的执行时间。多线程执行多出来的那点时间就是获取锁和释放锁消耗的时间。
那如何实现高并发呢?
答案是使用多进程。前面的文章有介绍多进程的使用
from multiprocessing import Pool
import time
COUNT = 50000000
def countdown(n):
while n > 0:
n -= 1
if __name__ == '__main__':
pool = Pool(processes=2)
start = time.time()
r1 = pool.apply_async(countdown, [COUNT // 2])
r2 = pool.apply_async(countdown, [COUNT // 2])
pool.close()
pool.join()
end = time.time()
print('Time taken in seconds -', end - start)
# 执行结果
# Time taken in seconds - 1.2389559745788574
使用多进程,每个进程运行250万次,大约消耗1.2秒的时间。差不多是上面线程版本的一半时间。
当然还可以使用其它Python
解析器,例如Jython
、IronPython
或PyPy
。
既然每个线程执行前都要获取锁,那么有一个线程获取到锁一直占用不释放,怎么办?
IO
密集型的程序会主动释放锁,但对于CPU
密集型的程序或IO
密集型和CPU
混合的程序,解析器将会如何工作呢?
早期的做法是Python
会执行100条指令后就强制线程释放GIL
让其它线程有可执行的机会。
可以通过以下获取到这个配置
>>> import sys
>>> sys.getcheckinterval()
100
在我的电脑中还打印了下面的输出警告
Warning (from warnings module):
File "__main__", line 1
DeprecationWarning: sys.getcheckinterval() and sys.setcheckinterval() are deprecated. Use sys.getswitchinterval() instead.
意思是sys.getcheckinterval()
方法已经废弃,应该使用sys.getswitchinterval()
方法。
因为传统的实现中每解析100指令的就强制线程释放锁的做法,会导致CPU
密集型的线程会一直占用GIL
而IO
密集型的线程会一直得不到解析的问题。于是新的线程切换方案就被提出来了
>>> sys.getswitchinterval()
0.005
这个方法返回0.05秒,意思是每个线程执行0.05秒后就释放GIL
,用于线程的切换。
0x04 总结
在CPython
解析器的实现由于global interpreter lock
(全局解释锁)的存在,任何时刻都只有一个线程能执行Python
的bytecode
(字节码)。
常见的内存管理方案有引用计数和垃圾回收,Python
选择了前者,这保证了单线程的执行效率,同时对编码实现也更加简单。想要移除GIL
是不容易的,即使成功将GIL
去除,对Python
的来说是牺牲了单线程的执行效率。
Python
中GIL
对IO
密集型程序可以较好的支持多线程并发,然而对CPU
密集型程序来说就要使用多进程或使用其它不使用GIL
的解析器。
目前最新的解析器实现中线程每执行0.05秒就会强制释放GIL
,进行线程的切换。
0x05 为了看懂GIL我阅读了下面这些资料
- https://docs.python.org/3.7/glossary.html#term-global-interpreter-lock
GIL - https://docs.python.org/3.7/glossary.html#term-bytecode
bytecode字节码 - https://github.com/python/cpython
CPython 源码 - https://wiki.python.org/moin/GlobalInterpreterLock
- https://realpython.com/python-gil/
What is the Python Global Interpreter Lock (GIL)? - http://dabeaz.blogspot.com/2010/01/python-gil-visualized.html
- https://mail.python.org/pipermail/python-dev/2009-October/093321.html
- https://www.youtube.com/watch?v=Obt-vMVdM8s&feature=youtu.be
Understanding the Python GIL - https://en.wikipedia.org/wiki/Global_interpreter_lock
你是否真的了解全局解析锁(GIL)的更多相关文章
- python 线程队列、线程池、全局解释器锁GIL
一.线程队列 队列特性:取一个值少一个,只能取一次,没有值的时候会阻塞,队列满了,也会阻塞 queue队列 :使用import queue,用法与进程Queue一样 queue is especial ...
- 全局解释器锁GIL
我们使用高并发,一次是创建1万个线程去修改一个数并打印结果看现象: from threading import Thread import os def func(args): global n n ...
- 全局解释器锁GIL & 线程锁
1.GIL锁(Global Interpreter Lock) Python代码的执行由Python虚拟机(也叫解释器主循环)来控制.Python在设计之初就考虑到要在主循环中,同时只有一个线程在执行 ...
- python 什么是全局解释器锁GIL
什么是全局解释器锁GIL Python代码的执行由Python 虚拟机(也叫解释器主循环,CPython版本)来控制,Python 在设计之初就考虑到要在解释器的主循环中,同时只有一个线程在执行,即在 ...
- python 全局解释锁GIL
Python的全局解释器锁GIL用于保护python解释器,使得任意时刻,只有一个线程在解释器中运行.从而保证线程安全 在多线程环境中,Python 虚拟机按以下方式执行: 1. 设置GIL2. 切换 ...
- 并发编程——全局解释器锁GIL
1.全局解释器锁GIL GIL其实就是一把互斥锁(牺牲了效率但是保证了数据的安全). 线程是执行单位,但是不能直接运行,需要先拿到python解释器解释之后才能被cpu执行 同一时刻同一个进程内多个线 ...
- 21.线程,全局解释器锁(GIL)
import time from threading import Thread from multiprocessing import Process #计数的方式消耗系统资源 def two_hu ...
- Python全局解释器锁 -- GIL
首先强调背景: 1.GIL是什么?GIL的全称是Global Interpreter Lock(全局解释器锁),来源是python设计之初的考虑,为了数据安全所做的决定. 2.每个CPU在同一时间只能 ...
- python 多线程编程之使用进程和全局解释器锁GIL
本文主要介绍如何在python中使用线程. 全局解释器锁: python代码的执行是由python虚拟机(又名解释器主循环)进行控制的.python中,主循环中同时只能有一个控制线程在执行,就像单核C ...
随机推荐
- Prim算法与Kruskal(没有代码)
两个最小生成树算法, 都有一个共同的思想: 这棵树是一点一点长大的; 并且每次生长, 都是贪心的. 不同之处是: Kruscal算法是以边为中心的, 每次找最小的并且有用的边添加到树上; Prim算法 ...
- c++小游戏——彩票
#include <cstdlib> #include <iostream> #include <cstdio> #include <cmath> #i ...
- 个人永久性免费-Excel催化剂功能第105波-批量调整不规范的图形对象到单一单元格内存储
在日常制表过程中,一个不得不面对的问题,许多的工作起点是基于其他人加工过的表格,无论自己多大的本领,面对不规范的其他人的制作的表格,经过自己的手,该擦的屁股还是要自己去亲手去擦,而带出来的也只会是一堆 ...
- Excel催化剂图表系列之一键完成IBCS国际商业标准图表
在数据分析领域,最后一公里的图表输出,是一片十分广阔的领域.一直以来,笔者深知不是这一方面的能手,学习上也仅仅是浅尝而止.没有往其深入研究并有所产出.很幸运地在数据圈子能够结识到其他的志同道合的伙伴, ...
- [小米OJ] 11. 构建短字符串
思路 排序后对两个字符串遍历 function solution(line) { var str = line.split(" "); var str1 = str[0].spli ...
- Flutter学习笔记(11)--文本组件、图标及按钮组件
如需转载,请注明出处:Flutter学习笔记(10)--容器组件.图片组件 文本组件 文本组件(text)负责显示文本和定义显示样式,下表为text常见属性 Text组件属性及描述 属性名 类型 默认 ...
- Java--随机数和随机数种子(转)
在计算机中并没有一个真正的随机数发生器,但是可以做到使产生的数字重复率很低,这样看起来好象是真正的随机数,实现这一功能的程序叫伪随机数发生器. 有关如何产生随机数的理论有许多,如果要详细地讨论,需要厚 ...
- iOS 类知乎”分页”效果的实现?
我们先看张gif图看一下效果(LICEcap录制的有点卡, 凑合看) 好像还是卡, 怼个视频演示链接吧: https://m.weibo.cn/1990517135/4398431764047996 ...
- gRPC in ASP.NET Core 3.0 -- Protocol Buffer(1)
现如今微服务很流行,而微服务很有可能是使用不同语言进行构建的.而微服务之间通常需要相互通信,所以微服务之间必须在以下几个方面达成共识: 需要使用某种API 数据格式 错误的模式 负载均衡 ... 现在 ...
- jQuery中事件与动画
jQuery中的事件与动画 一. jQuery中的事件 jQuery事件是对javaScript事件的封装. 1.基础事件 在javaScript中,常用的基础事件有鼠标事件.键盘事件.windo ...