GIL锁、进程池与线程池
1.什么是GIL?
官方解释:
'''
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple
native threads from executing Python bytecodes at once. This lock is necessary mainly
because CPython’s memory management is not thread-safe. (However, since the GIL
exists, other features have grown to depend on the guarantees that it enforces.)
''' 释义:
在CPython中,这个全局解释器锁,也称为GIL,是一个互斥锁,防止多个线程在同一时间执行Python字节码,这个锁是非常重要的,
因为CPython的内存管理非线程安全的,很多其他的特性依赖于GIL,所以即使它影响了程序效率也无法将其直接去除 总结:
在CPython中,GIL会把线程的并行变成串行,导致效率降低
GIL指的是全局解释器锁,本质就是一把互斥锁,是加在解释器上的互斥锁。首先需要明确的是GIL并不是Python的特性,它是在实现CPython解释器时所引入的概念,GIL也仅仅存在于CPython中,解释器并不是只有CPython,还有PyPy,JPython等等,这些就不依赖GIL。
2.为什么需要GIL?
由于CPython的内存管理是非线程安全的,于是CPython就给解释器加了GIL锁,解决了安全问题,但是降低了效率(即使在多核处理器下,也无法实现真正的并行)。另外虽然有解决的方案,但是CPython应用比较广泛,牵扯太多,一旦修改,那么很多以前的基于GIL的程序都需要修改,所以这个问题就变成了历史遗留问题。
GIL与GC的孽缘
在使用Python中进行编程时,程序员无需参与内存的管理工作,这是因为Python有自带的内存管理机制,简称GC。那么GC与GIL有什么关联?
要搞清楚这个问题,需先了解GC的工作原理,Python中内存管理使用的是引用计数,每个数会被加上一个整型的计数器,表示这个数据被引用的次数,当这个整数变为0时则表示该数据已经没有人使用,成了垃圾数据。
当内存占用达到某个阈值时,GC会将其他线程挂起,然后执行垃圾清理操作,垃圾清理也是一串代码,也就需要一条线程来执行。
示例代码:
from threading import Thread
def task():
a = 10
print(a)
# 开启三个子线程执行task函数
Thread(target=task).start()
Thread(target=task).start()
Thread(target=task).start()
上述代码内存结构如下:
通过上图可以看出,GC与其他线程都在竞争解释器的执行权,而CPU何时切换,以及切换到哪个线程都是无法预支的,这样一来就造成了竞争问题,假设线程1正在定义变量a=10,而定义变量第一步会先到到内存中申请空间把10存进去,第二步将10的内存地址与变量名a进行绑定,如果在执行完第一步后,CPU切换到了GC线程,GC线程发现10的地址引用计数为0则将其当成垃圾进行了清理,等CPU再次切换到线程1时,刚刚保存的数据10已经被清理掉了,导致无法正常定义变量。
当然其他一些涉及到内存的操作同样可能产生问题问题,为了避免GC与其他线程竞争解释器带来的问题,CPython简单粗暴的给解释器加了互斥锁,如下图所示:
有了GIL后,多个线程将不可能在同一时间使用解释器,从而保证了解释器的数据安全。
GIL的加锁与解锁时机
加锁的时机:在调用解释器时立即加锁
解锁时机:
当前线程遇到了IO时释放
当前线程执行时间超过设定值时释放,解释器会检测线程的执行时间,一旦到达某个阈值,通知线程保存状态切换线程,以此来保证数据安全
3.GIL带来的问题
首先必须明确执行一个py文件,分为三个步骤
从硬盘加载Python解释器到内存
从硬盘加载py文件到内存
解释器解析py文件内容,交给CPU执行
其次需要明确的是每当执行一个py文件,就会立即启动一个python解释器,
当执行test.py时其内存结构如下:
GIL,叫做全局解释器锁,加到了解释器上,并且是一把互斥锁,那么这把锁对应用程序到底有什么影响?
这就需要知道解释器的作用,以及解释器与应用程序代码之间的关系
py文件中的内容本质都是字符串,只有在被解释器解释时,才具备语法意义,解释器会将py代码翻译为当前系统支持的指令交给系统执行。
当进程中仅存在一条线程时,GIL锁的存在没有不会有任何影响,但是如果进程中有多个线程时,GIL锁就开始发挥作用了。
开启子线程时,给子线程指定了一个target表示该子线程要处理的任务即要执行的代码。代码要执行则必须交由解释器,即多个线程之间就需要共享解释器,为了避免共享带来的数据竞争问题,于是就给解释器加上了互斥锁!
由于互斥锁的特性,程序串行,保证数据安全,降低执行效率,GIL将使得程序整体效率降低!
4.GIL的性能讨论
GIL的优点:
保证了CPython中的内存管理是线程安全的
GIL的缺点:
互斥锁的特性使得多线程无法并行
但我们并不能因此就否认Python这门语言,其原因如下:
GIL仅仅在CPython解释器中存在,在其他的解释器中没有,并不是Python这门语言的缺点
在单核处理器下,多线程之间本来就无法真正的并行执行
在多核处理下,运算效率的确是比单核处理器高,但是要知道现代应用程序多数都是基于网络的(qq,微信,爬虫,浏览器等等),CPU的运行效率是无法决定网络速度的,而网络的速度是远远比不上处理器的运算速度,则意味着每次处理器在执行运算前都需要等待网络IO,这样一来多核优势也就没有那么明显了
举个例子:
任务1 从网络上下载一个网页,等待网络IO的时间为1分钟,解析网页数据花费,1秒钟
任务2 将用户输入数据并将其转换为大写,等待用户输入时间为1分钟,转换为大写花费,1秒钟
单核CPU下:1.开启第一个任务后进入等待。2.切换到第二个任务也进入了等待。一分钟后解析网页数据花费1秒解析完成切换到第二个任务,转换为大写花费1秒,那么总耗时为:1分+1秒+1秒 = 1分钟2秒
多核CPU下:1.CPU1处理第一个任务等待1分钟,解析花费1秒钟。1.CPU2处理第二个任务等待1分钟,转换大写花费1秒钟。由于两个任务是并行执行的所以总的执行时间为1分钟+1秒钟 = 1分钟1秒
可以发现,多核CPU对于总的执行时间提升只有1秒,但是这边的1秒实际上是夸张了,转换大写操作不可能需要1秒,时间非常短!
上面的两个任务都是需要大量IO时间的,这样的任务称之为IO密集型,与之对应的是计算密集型即没有IO操作全都是计算任务。
对于计算密集型任务,Python多线程的确比不上其他语言!为了解决这个弊端,Python推出了多进程技术,可以良好的利用多核处理器来完成计算密集任务。
from multiprocessing import Process
from threading import Thread
import os,time
def work():
res=0
for i in range(100000000):
res*=i if __name__ == '__main__':
l=[]
print(os.cpu_count()) #本机为4核
start=time.time()
for i in range(4):
p=Process(target=work) #耗时5s多
p=Thread(target=work) #耗时18s多
l.append(p)
p.start()
for p in l:
p.join()
stop=time.time()
print('run time is %s' %(stop-start)) 计算密集型:多进程效率高
计算密集型,多进程效率高
from multiprocessing import Process
from threading import Thread
import threading
import os,time
def work():
time.sleep(2)
print('===>') if __name__ == '__main__':
l=[]
print(os.cpu_count()) #本机为4核
start=time.time()
for i in range(400):
# p=Process(target=work) #耗时12s多,大部分时间耗费在创建进程上
p=Thread(target=work) #耗时2s多
l.append(p)
p.start()
for p in l:
p.join()
stop=time.time()
print('run time is %s' %(stop-start)) I/O密集型:多线程效率高
I/O密集型,多线程效率高
总结:
1.在单核情况下,无论是I/O密集型还是计算密集型,GIL都不会产生影响。
2.如果是多核情况下,I/O密集型会受到GIL的影响,但是很明显I/O的速度比计算速度慢,所以影响不大。
3.I/O密集型用多线程(线程开销小,节省资源),计算密集型使用多进程(因为CPython多线程是无法并行的)。
另外之所以广泛采用CPython解释器,就是因为大量的应用程序都是I/O密集型的,还有一个重要的原因是CPython可以无缝对接各种C语言实现的库,对于一些数学计算相关的应用程序就可以直接使用各种现成的算法。
5.GIL与自定义线程锁的区别
GIL保护的是解释器级别的数据安全,比如对象的引用计数,垃圾分代数据等等(垃圾回收机制)。
对于程序中自己定义的数据没有任何的保护效果,这一点在没有介绍GIL前我们就已经知道了,所以当程序中出现了共享自定义的数据时就要自己加锁,如下例:
from threading import Thread,Lock
import time a = 0
def task():
global a
temp = a
time.sleep(0.01)
a = temp + 1 t1 = Thread(target=task)
t2 = Thread(target=task)
t1.start()
t2.start()
t1.join()
t2.join()
print(a)
过程分析:
1.线程1获得CPU执行权,并获取GIL锁执行代码,得到a的值为0后进入睡眠,释放CPU并释放GIL。
2.线程2获得CPU执行权,并获取GIL锁执行代码,得到a的值为0后进入睡眠,释放CPU并释放GIL。
3.线程1睡醒后获得CPU执行权,并获取GIL执行代码 ,将temp的值0+1后赋给a,执行完毕释放CPU并释放GIL。
4.线程2睡醒后获得CPU执行权,并获取GIL执行代码 ,将temp的值0+1后赋给a,执行完毕释放CPU并释放GIL,最后a的值也就是1。
之所以出现问题是因为两个线程在并发的执行同一段代码,解决方案就是加锁!
from threading import Thread,Lock
import time lock = Lock()
a = 0
def task():
global a
lock.acquire()
temp = a
time.sleep(0.01)
a = temp + 1
lock.release() t1 = Thread(target=task)
t2 = Thread(target=task)
t1.start()
t2.start()
t1.join()
t2.join()
print(a)
过程分析:
1.线程1获得CPU执行权,并获取GIL锁执行代码 ,得到a的值为0后进入睡眠,释放CPU并释放GIL,不释放lock
2.线程2获得CPU执行权,并获取GIL锁,尝试获取lock失败,无法执行,释放CPU并释放GIL
3.线程1睡醒后获得CPU执行权,并获取GIL继续执行代码 ,将temp的值0+1后赋给a,执行完毕释放CPU释放GIL,释放lock,此时a的值为1
4.线程2获得CPU执行权,获取GIL锁,尝试获取lock成功,执行代码,得到a的值为1后进入睡眠,释放CPU并释放GIL,不释放lock
5.线程2睡醒后获得CPU执行权,获取GIL继续执行代码 ,将temp的值1+1后赋给a,执行完毕释放CPU释放GIL,释放lock,此时a的值为2
6.进程池与线程池
1.什么是进程/线程池?
池表示一个容器,本质上就是一个存储进程或线程的列表。
2.何时使用进程池、线程池?
I/O密集型任务使用线程池,计算密集型任务使用进程池。
3.为什么需要进程/线程池?
在很多情况下需要控制进程或线程的数量在一个合理的范围,例如TCP程序中,一个客户端对应一个线程,虽然线程的开销小,但肯定不能无限的开,否则系统资源迟早被耗尽,解决的办法就是控制线程的数量。
线程/进程池不仅帮我们控制线程/进程的数量,还帮我们完成了线程/进程的创建,销毁,以及任务的分配。
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
import time,os # 创建进程池,指定最大进程数为3,此时不会创建进程,不指定数量时,默认为CPU和核数
pool = ProcessPoolExecutor(3) def task():
time.sleep(1)
print(os.getpid(),"working..") if __name__ == '__main__':
for i in range(10):
pool.submit(task) # 提交任务时立即创建进程 # 任务执行完成后也不会立即销毁进程
time.sleep(2) for i in range(10):
pool.submit(task) #再有新任务是 直接使用之前已经创建好的进程来执行
进程池的使用
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
from threading import current_thread,active_count
import time,os # 创建进程池,指定最大线程数为3,此时不会创建线程,不指定数量时,默认为CPU和核数*5
pool = ThreadPoolExecutor(3)
print(active_count()) # 只有一个主线 def task():
time.sleep(1)
print(current_thread().name,"working..") if __name__ == '__main__':
for i in range(10):
pool.submit(task) # 第一次提交任务时立即创建线程 # 任务执行完成后也不会立即销毁
time.sleep(2) for i in range(10):
pool.submit(task) #再有新任务是 直接使用之前已经创建好的线程来执行
线程池的使用
7.同步与异步
同步和异步指的是提交任务的方式。
同步(调用/执行/任务/提交):发起任务后必须等待任务结束,拿到一个结果才能继续执行。
异步:发起任务后不需要关心任务的执行过程,可以继续往下运行。
异步的效率高于同步,但是并不是所有任务都可以异步执行,判断一个任务是否可以异步的条件是:任务发起方是否立即需要执行结果。
同步会有等待的效果,但是,这和阻塞时完全不同的,阻塞时程序会被剥夺CPU执行权,而同步调用则不会,因为同步在等待任务执行结束,得到结果,CPU还在执行中。没有切换。
from concurrent.futures import ThreadPoolExecutor
from threading import current_thread
import time pool = ThreadPoolExecutor(3)
def task(i):
time.sleep(0.01)
print(current_thread().name,"working..")
return i ** i if __name__ == '__main__':
objs = []
for i in range(3):
res_obj = pool.submit(task,i) # 异步方式提交任务# 会返回一个对象用于表示任务结果
objs.append(res_obj) # 该函数默认是阻塞的 会等待池子中所有任务执行结束后执行
pool.shutdown(wait=True) # 从结果对象中取出执行结果
for res_obj in objs:
print(res_obj.result())
print("over")
异步调用并获取结果
from concurrent.futures import ThreadPoolExecutor
from threading import current_thread
import time pool = ThreadPoolExecutor(3)
def task(i):
time.sleep(0.01)
print(current_thread().name,"working..")
return i ** i if __name__ == '__main__':
objs = []
for i in range(3):
res_obj = pool.submit(task,i) # 会返回一个对象用于表示任务结果
print(res_obj.result()) #result是同步的一旦调用就必须等待 任务执行完成拿到结果
print("over")
同步调用并获取结果
GIL锁、进程池与线程池的更多相关文章
- GIL解释锁及进程池和线程池
官方介绍 ''' 定义: In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple nati ...
- GIL锁、进程池与线程池、同步异步
GIL锁定义 GIL锁:Global Interpreter Lock 全局解释器 本质上是一把互斥锁 官方解释: 在CPython中,这个全局解释器锁,也称为GIL,是一个互斥锁,防止多个线程在同 ...
- GIL全局解释器锁、死锁现象、python多线程的用处、进程池与线程池理论
昨日内容回顾 僵尸进程与孤儿进程 # 僵尸进程: 所有的进程在运行结束之后并不会立刻销毁(父进程需要获取该进程的资源) # 孤儿进程: 子进程正常运行 但是产生该子进程的父进程意外死亡 # 守护进程: ...
- GIL解释器锁 & 进程池与线程池
今日内容 GIL 全局解释器锁(重要理论) 验证 GIL 的存在及功能 验证 python 多线程是否有用 死锁现象 进程池与线程池(使用频率高) IO模型 详细参考: https://www.bil ...
- GIL与普通互斥锁区别,死锁现象,信号量,event事件,进程池与线程池,协程
GIL与普通互斥锁区别 GIL锁和互斥锁的异同点 相同: 都是为了解决解释器中多个线程资源竞争的问题 异: 1.互斥锁是Python代码层面的锁,解决Python程序中多线程共享资源的问题(线程数据共 ...
- 第三十八天 GIL 进程池与线程池
今日内容: 1.GIL 全局解释器锁 2.Cpython解释器并发效率验证 3.线程互斥锁和GIL对比 4.进程池与线程池 一.全局解释器锁 1.GIL:全局解释器锁 GIL本质就是一把互斥锁,是夹在 ...
- GIL,queue,进程池与线程池
GIL 1.什么是GIL(这是Cpython解释器) GIL本质就是一把互斥锁,既然是互斥锁,原理都是一样的,都是让多个并发线程同一时间只能有一个执行 即:有了GIL的存在,同一进程内的多个线程同一时 ...
- day37 GIL、同步、异步、进程池、线程池、回调函数
1.GIL 定义: GIL:全局解释器锁(Global Interpreter Lock) 全局解释器锁是一种互斥锁,其锁住的代码是全局解释器中的代码 为什么需要全局解释器锁 在我们进行代码编写时,实 ...
- 4月27日 python学习总结 GIL、进程池、线程池、同步、异步、阻塞、非阻塞
一.GIL:全局解释器锁 1 .GIL:全局解释器锁 GIL本质就是一把互斥锁,是夹在解释器身上的, 同一个进程内的所有线程都需要先抢到GIL锁,才能执行解释器代码 2.GIL的优缺点: 优点: 保 ...
随机推荐
- 直径上的乱搞 bzoj1999求树直径上的结点+单调队列,bzoj1912负权树求直径+求直径边
直径上的乱搞一般要求出这条直径上的点集或者边集 bzoj1999:对直径上的点集进行操作 /* 给出一颗树,在树的直径上截取长度不超过s的路径 定义点u到s的距离为u到s的最短路径长度 定义s的偏心距 ...
- 在lnmp环境下,将原来的PHP7.0升级到PHP7.2
基础环境: 系统:centos6.8 环境:lnmp 停止PHP7.0的版本,在做如下操作: 1.下载php-7.2.6.tar.bz2软件包放在/opt 路径下 mkdir /usr/local ...
- easyui合并多个单元格
$('#table-v2').datagrid({ url: './data/am/data1_table.json', pagination: true, //显示分页 fit: true, //d ...
- appium+python测试app使用相对坐标定位元素
我们获取到的是绝对坐标,如果换一个屏幕分辨率不同的手机那这个坐标自然会发生变化,要实现不同手机均能实现点击同一控件自然要用到相对坐标了,具体方法如下: 1.获取当前空间的绝对坐标(x1,y1),开启指 ...
- Windows批处理命令用法
阅读下面文字需要一定的dos基础概念,象:盘符.文件.目录(文件夹).子目录.根目录.当前目录 每个命令的完整说明请加 /? 参数参考微软的帮助文档可以看到,在 /? 帮助里,"命令扩展名& ...
- docker批量删除none镜像
1.直接用docker images | grep none | awk ‘{print $3}’ |xgars docker rmi 通过关键字搜索,得到docker id,进行删除
- vue自定义指令directives使用及生命周期
生命周期 bind:只调用一次,指令第一次绑定到元素时调用,用这个钩子函数可以定义一个绑定时执行一次的初始化动作. inserted:被绑定元素插入父节点时调用(父节点存在即可调用,不必存在于docu ...
- Description Resource Path Location Type Project configuration is not up-to-date with pom.xml. Select: Maven->Update Project... from the project context menu or use Quick Fix. spark-MT line 1 Maven Co
1.相信大家新建的maven项目,然后添加好依赖(即修改了pom.xml文件以后就会出现如下所示的错误): Description Resource Path Location Type Projec ...
- Select2 多层次赋值时异步赋值的问题
场景: 当选择人员时加载人员,选择部门时加载部门.所以在人员下,选择人员A后,如果选择部门,会触发二级select 重新获取数据. 问题: 使用select2()方法进行绑定远程数据后,对第二个sel ...
- 盘点那些Vs中常用到的Tab快捷编码
1.快速声明for循环:for+Tab 2.快速声明Foreach遍历:foreach+Tab 3.快速定义属性:prop+Tab 4.