今日内容:
1、GIL全局解释器锁
2、GIL vs 自定义互斥锁
3、死锁现象与递归锁
4、信号量Semaphore
5、Event事件
6、线程queue
一、GIL全局解释器锁
1、什么是GIL全局解释器锁?
GIL本质就是一把互斥锁,相当于执行权限,每个进程内都会存在一把GIL,同一进程内的多个线程
必须抢到GIL之后才能使用Cpython解释器来执行自己的代码,即同一进程下的多个线程无法实现并行
但是可以实现并发
注:在Cpython解释器下,如果想实现并行可以开启多个进程
2、为何要有GIL?
我们首先要知道,一个多线程是怎么执行的,假设在一个进程中有三个线程,线程中是要运行的代码
①如果要运行代码,就必须要先获得CPython解释器的权限才能将代码交由解释器翻译成CPU可以理解的语言
②将翻译好的代码交由操作系统,由操作系统交给CPU执行运算
由于每个进程内都会存在一把GIL,同一进程内的多个线程,必须抢到GIL之后才能使用Cpython解释器来执行自己的代码,即同一进程下的多个线程无法实现并行,但是可以实现并发
那么我们反过来想一下,如果没有GIL的存在,那么多个线程就变成了并行的,要知道解释器中有一个垃圾回收机制,其实也是一个线程,也变成了并行,就会造成一种情况的发生,对于同一个数据100,可能线程1执行x=100的同时,而垃圾回收执行的是回收100的操作,造成了数据的丢失。
所以,为了CPython解释器的垃圾回收机制的线程安全,就必须使用GIL!
3、如何用GIL?
有了GIL,应该如何处理并发
我们有四个任务需要处理,处理方式肯定是要玩出并发的效果,解决方案可以是:
方案一:开启四个进程
方案二:一个进程下,开启四个线程
单核情况下,分析结果:
如果四个任务是计算密集型,没有多核来并行计算,方案一徒增了创建进程的开销,方案二胜
如果四个任务是I/O密集型,方案一创建进程的开销大,且进程的切换速度远不如线程,方案二胜
多核情况下,分析结果:
如果四个任务是计算密集型,多核意味着并行计算,在python中一个进程中同一时刻只有一个线程执行用不上多核,方案一胜
如果四个任务是I/O密集型,再多的核也解决不了I/O问题,方案二胜
结论:现在的计算机基本上都是多核,python对于计算密集型的任务开多线程的效率并不能带来多大性能上的提升,甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。
计算密集型:
from multiprocessing import Process from threading import Thread import os,time def task(): 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=task) #耗时16.226743459701538s p=Thread(target=task) #耗时26.44382882118225s l.append(p) p.start() for p in l: p.join() stop=time.time() print('run time is %s' %(stop-start))
I/O密集型:
from multiprocessing import Process from threading import Thread import os,time def task(): time.sleep(2) if __name__ == '__main__': l=[] print(os.cpu_count()) #本机为4核 start=time.time() for i in range(400): # p=Process(target=task) #耗时29.650749683380127s p=Thread(target=task) #耗时2.0773582458496094s l.append(p) p.start() for p in l: p.join() stop=time.time() print('run time is %s' %(stop-start))
二、GIL vs 自定义互斥锁
GIL保护的是解释器级别的数据,但是用户自己的数据需要自己加锁处理
from threading import Thread,Lock import time mutex=Lock() n=100 def task(): global n with mutex: temp=n time.sleep(0.1) n=temp-1 if __name__ == '__main__': l=[] for i in range(100): t=Thread(target=task) l.append(t) t.start() for t in l: t.join() print(n)
通过自定义互斥锁,每个线程除了要抢到GIL锁之外还要抢到自定义的锁,否则即使抢到了GIL也没有用,这样就充分保证了数据的安全性。
三、死锁现象与递归锁
死锁:两个或两个以上的进程或者线程在执行过程中,因为真多资源二造成的互相等待现象,若无外力的作用,题目都将一直处于阻塞状态,这些互相等待的进程或者线程就被称为死锁。
from threading import Thread,Lock import time mutexA=Lock() mutexB=Lock() class MyThread(Thread): def run(self): self.func1() self.func2() def func1(self): mutexA.acquire() print('\033[41m%s 拿到A锁\033[0m' %self.name) mutexB.acquire() print('\033[42m%s 拿到B锁\033[0m' %self.name) mutexB.release() mutexA.release() def func2(self): mutexB.acquire() print('\033[43m%s 拿到B锁\033[0m' %self.name) time.sleep(2) mutexA.acquire() print('\033[44m%s 拿到A锁\033[0m' %self.name) mutexA.release() mutexB.release() if __name__ == '__main__': for i in range(10): t=MyThread() t.start() ''' Thread-1 拿到A锁 Thread-1 拿到B锁 Thread-1 拿到B锁 Thread-2 拿到A锁 然后就卡住,死锁了 '''
解决方法,使用递归锁(RLock)
这个RLock内部有一个Lock和一个counter变量,counter记录着acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁:
from threading import Thread,Lock,RLock import time mutexB=mutexA=RLock() #一个线程拿到锁,counter加1,该线程内又碰到加锁的情况,则counter继续加1,这期间所有其他线程都只能等待,等待该线程释放所有锁,即counter递减到0为止 class Mythead(Thread): def run(self): self.f1() self.f2() def f1(self): mutexA.acquire() print('%s 抢到A锁' %self.name) mutexB.acquire() print('%s 抢到B锁' %self.name) mutexB.release() mutexA.release() def f2(self): mutexB.acquire() print('%s 抢到了B锁' %self.name) time.sleep(2) mutexA.acquire() print('%s 抢到了A锁' %self.name) mutexA.release() mutexB.release() if __name__ == '__main__': for i in range(100): t=Mythead() t.start()
四、信号量Semaphore
Semaphore管理一个内置的计数器,
每当调用acquire()时内置计数器-1;
调用release() 时内置计数器+1;
计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。
from threading import Thread,Semaphore import time,random sm=Semaphore(5)#最大连接数为5 def task(name): sm.acquire() print('%s 正在上厕所' %name) time.sleep(random.randint(1,3)) sm.release() if __name__ == '__main__': for i in range(20): t=Thread(target=task,args=('路人%s' %i,)) t.start()
五、Event事件
线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果程序中的其他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时就需要用到threading中的Event对象。对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在 初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行
from threading import Thread,Event import time event=Event() def light(): print('红灯正亮着') time.sleep(3) event.set() #绿灯亮 def car(name): print('车%s正在等绿灯' %name) event.wait() #等灯绿 print('车%s通行' %name) if __name__ == '__main__': # 红绿灯 t1=Thread(target=light) t1.start() # 车 for i in range(10): t=Thread(target=car,args=(i,)) t.start()
六、线程queue
线程的queue和进程一样,这里补充一下queue.LifoQueue()和queue.PriorityQueue()优先级
queue.LifoQueue() 后进先出---->堆栈
q=queue.LifoQueue(3) q.put(1) q.put(2) q.put(3) print(q.get())#3 print(q.get())#2 print(q.get())#1
queue.PriorityQueue() 设置优先级别,数字越小,优先级别越高
q=queue.PriorityQueue(3) #优先级,优先级用数字表示,数字越小优先级越高 q.put((10,'a')) q.put((-1,'b')) q.put((100,'c')) print(q.get())#(-1, 'b') print(q.get())#(10, 'a') print(q.get())#(100, 'c')