文章目录
- GIL全局解释器锁与死锁现象
- GIL简介
- GIL的作用
- 总结
- 扩展阅读
- 验证GIL的存在
- IO密集型与计算密集型
- IO密集型
- 计算密集型
- 死锁现象
- 死锁现象的解决方法(递归锁RLock)
- python多线程是否有用
- IO密集型演示
- 计算密集型演示
GIL全局解释器锁与死锁现象
GIL简介
在Python中,可以通过多进程、多线程和多协程来实现多任务。 在多线程的实现过程中,为了避免出现资源竞争问题,可以使用互斥锁来使线程同步(按顺序)执行。
而GIL(Global Interperter Lock),称为全局解释器锁,本质上就是一个内置互斥锁,主要用于阻止同一个进程下的多个线程同时被允许(python的多线程无法使用多核优势),没有获得GIL锁,线程就不能执行
需要注意的是,GIL只存在于我们平时常用的Cpython解释器中,其他如Jpython,Ipython,PyPy,IronPython中都没有
其存在的主要原因在于Cpython解释器的内存管理不是线程安全的
并且在调用外部代码(如 C/C++扩展函数)的时候,GIL将会被锁定,直到这个函数结束为止(由于在这期间没有Python的字节码被运行,所以不会做线程切换)编写扩展的程序员可以主动解锁GIL。
GIL的作用
- Python中的多线程被称为“伪多线程”,因为无论如何,都逃不过GIL解释器锁。
- 因为GIL的存在,在Python中同一时刻有且只有一个线程会执行。
- 因为线程是存在于进程中的,线程是CPU调度和分派的基本单位,Python中的多线程由于GIL锁的存在无法利用多核 CPU。
- GIL在程序中有IO操作时才切换到其他线程,所以Python中的多线程不适合计算密集型的程序,只适合IO密集型的程序。
总结
- GIL是Cpython解析器的特点
- python同一个进程内的多个线程无法利用多核优势(不能并行但是可以并发)
- 同一个进程内的多个线程要想运行必须先抢GIL锁
- 所有的解释性语言几乎都无法实现同一个进程下的多个线程同时被运行
扩展阅读
python设计之初还无多核概念,是单核。 python需要做垃圾回收 如何回收:垃圾回收线程,启动并进行垃圾回收 垃圾回收线程在回收垃圾之时,若有多条线程,比如现已有一条A线程,此时启动垃圾回收线程,但在销毁变量时,线程A还在使用变量,垃圾回收机制便无法执行。所以垃圾回收机制一旦启动就需要一把锁。这把锁就叫做全局解释器锁GIL,垃圾回收线程获得此锁之后,其他线程就不能运行,那么在回收变量的时候就不会出现并发安全问题。 垃圾回收线程在运行的时候,其他线程均处于阻塞状态。 只有拿到GIL锁的线程,才能运行。 同一个时刻,在一个进程中可以开多个线程,但只能有一条线程在执行
起初设置GIL仅仅是为了做垃圾回收,在当时还只有单核,并无多核。所以开了多条线程也不会被多个CPU调动执行,因为只有单核。线程拿到这把锁才能运行。在当时是没有问题,因为只有一个CPU。但是随着多核CPU的出现,假设电脑是四核,一个进程四条线程,理论一个线程一核,但是Python不行,在一个线程中开四条线程并不会被四个核运行,同一时刻只能有一个线程在一个核运行,就是因为GIL原因。
因此python不能利用多核优势是Cpython解释器的问题。
验证GIL的存在
from threading import Thread
import time
m = 100
def test():
global m
# 防止措施,防止计算过慢出错
tmp = m
# time.sleep(0.1) # 模拟IO操作
tmp -= 1
m = tmp
# m -= 1
# 循环开启线程
for i in range(100):
t = Thread(target=test)
t.start()
time.sleep(1) # 用来防止CPU计算过慢
print(m) # 0 执行time.sleep(0.1)时: 结果为99
"""
同一个进程下的多个线程虽然有GIL的存在不会出现并行的效果
但是如果线程内有IO操作还是会造成数据的错乱 这个时候需要我们额外的添加互斥锁
GIL只能确保线程在同一时间不能运行,不能保证线程内的数据是否安全
"""
IO密集型与计算密集型
IO密集型
大部分的程序在运行时,都需要大量IO操作,比如网络数据的收发,大文件的读写,这样的程序称为IO密集型程序。 IO密集型程序在运行时,需要大量的时间进行等待,如果IO操作不完成,程序无法执行后面的操作,一直处于等待状态,导致CPU空闲。 由于GIL的存在,同一时刻只能有一个线程执行,在程序进行IO操作时,CPU实际并没有做任何工作,程序执行效率非常低。 为了提高CPU的使用率,Python解释在程序执行IO等待时,会释放GIL锁,让其它线程执行,提高Python程序的执行效率。 所以,GIL对于IO密集型的影响很小,多线程适合用来做IO密集型的程序。
计算密集型
计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。
死锁现象
是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程
在多道程序系统中,由于多个进程的并发执行,改善了系统资源的利用率并提高了系统的处理能力。然而,多个进程的并发执行也带来了新的问题——死锁。所谓死锁是指多个进程因竞争资源而造成的一种僵局,若无外力作用,这些进程都将无法向前推进。
因此就算知道锁的特性及使用方式 也不要轻易的使用 因为容易产生死锁现象"
from threading import Thread, Lock
import time
A = Lock()
B = Lock()
class MyThread(Thread):
def run(self):
self.func1()
self.func2()
def func1(self):
A.acquire()
print('%s 抢到了A锁' % self.name) # current_thread().name 获取线程名称
B.acquire()
print('%s 抢到了B锁' % self.name)
time.sleep(1)
B.release()
print('%s 释放了B锁' % self.name)
A.release()
print('%s 释放了A锁' % self.name)
def func2(self):
B.acquire()
print('%s 抢到了B锁' % self.name)
A.acquire()
print('%s 抢到了A锁' % self.name)
A.release()
print('%s 释放了A锁' % self.name)
B.release()
print('%s 释放了B锁' % self.name)
for i in range(10):
obj = MyThread()
obj.start()
- 流程
线程1 先开始执行func1,分别拿到AB锁,然后释放 线程1 先执行func2,先拿到了B锁,开始sleep 线程2 先拿到了A锁 这时候形成了僵局,线程2想要线程1手里的B锁,线程1想要线程2里的A锁。
死锁现象的解决方法(递归锁RLock)
解决死锁问题可以用到RLock(可重入)模块,可以重复acquire,获得几次,就要释放几次
可以被连续的acquire和release 但是只能被第一个抢到这把锁指向上述操作 它的内部有一个计数器 每acquire一次计数加1 每release一次计数减1 只要计数不为零 那么其他人都无法抢到该锁
简单点说就是相当于添加了一个计数器在里面,只有当计数为0时,其他线程才能抢到GIL锁,否则就一直被占用
from threading import Thread, RLock
import time
# 指向同一个内存地址
A = RLock()
B = A
class MyThread(Thread):
def run(self):
self.func1()
self.func2()
def func1(self):
A.acquire()
print('%s 抢到了A锁' % self.name) # current_thread().name 获取线程名称
B.acquire()
print('%s 抢到了B锁' % self.name)
time.sleep(1)
B.release()
print('%s 释放了B锁' % self.name)
A.release()
print('%s 释放了A锁' % self.name)
def func2(self):
B.acquire()
print('%s 抢到了B锁' % self.name)
A.acquire()
print('%s 抢到了A锁' % self.name)
A.release()
print('%s 释放了A锁' % self.name)
B.release()
print('%s 释放了B锁' % self.name)
for i in range(10):
obj = MyThread()
obj.start()
python多线程是否有用
- 由于python属于是一门解释性语言,受Cpython解释器的影响,其多线程一直被人诟病,但是这是不是说明python多线程没有用呢。答案是否定的,存在即合理,python多线程肯定有用。接下来详细说说
python多线程是否有用,还需要看情况而定,前文可知,程序通常分为IO密集型和计算密集型,在python中遇到IO就需要切换CPU, 并且开设进程还需要申请内存空间和拷贝代码,因此在IO密集型的程序中,开设多线程是有巨大优势的。而在计算密集型程序中,开设多线程就没有必要了,因此需要开设多进程来利用多核心优势。因此,在程序中正确采取多进程和多线程相结合的方法,能更好的提升程序的效率。
IO密集型演示
from multiprocessing import Process
import os,time
def work():
time.sleep(2)
if __name__ == '__main__':
l=[]
print(os.cpu_count()) #本机为12核
start=time.time()
for i in range(400):
p=Process(target=work) #耗时10.91s多,大部分时间耗费在创建进程上
# p=Thread(target=work) #耗时2.06s多
l.append(p)
p.start()
for p in l:
p.join()
stop=time.time()
print('run time is %s' %(stop-start))
计算密集型演示
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()) # 本机为12核
start=time.time()
for i in range(6):
# p=Process(target=work) # 耗时6.76s多
p=Thread(target=work) # 耗时26.54s多
l.append(p)
p.start()
for p in l:
p.join()
stop=time.time()
print('run time is %s' %(stop-start))