1.全局解释器锁GIL
什么是GIL
GIL是全局解释器锁他规定了每个线程在被CPU执行前都要获得一个GIL,并且同一时刻只有一个线程被执行
为什么要有GIL
因为CPython解释器的内存管理不是线程安全的,所以在CPython中增加的一个GIL锁
线程释放GIL锁的情况
在I/O操作等会引起阻塞状态的操作时,会暂时释放GIL,其他线程可以获取GIL继续执行
python的多线程和多进程的使用场景
1.当CPU处于多任务计算密集型的情况下
单核:两者差不多,但是多线程更加省资源
多核:由于多线程不能利用多核的优势,所以使用多进程更有优势
2.当CPU处于多任务I/O密集型的情况下
单核:和上述一样,多线程更省资源
多核:由于I/O密集型的任务耗费的时间大部分在I/O操作上,并且cpu在阻塞状态时可以暂时释放GIL,所以还是多线程更省资源(开启进程是耗费资源,当需要进行的任务过多,多进程耗费的时间会大大超过多线程)
1 # 计算密集型 2 from multiprocessing import Process 3 import os,time 4 def work(): 5 res=0 6 for i in range(100000000): 7 res*=i 8 9 10 if __name__ == '__main__': 11 l=[] 12 print(os.cpu_count()) # 本机为8核 13 start=time.time() 14 for i in range(6): 15 p=Process(target=work) # 在当前机器上,多进程耗时 9.134473323822021s 16 # p=Thread(target=work) # 在当前机器上,多线程耗时 51.49368095397949s 17 l.append(p) 18 p.start() 19 for p in l: 20 p.join() 21 stop=time.time() 22 print('run time is %s' %(stop-start))
1 from multiprocessing import Process 2 import os,time 3 def work(): 4 time.sleep(2) 5 6 if __name__ == '__main__': 7 l=[] 8 print(os.cpu_count()) #本机为8核 9 start=time.time() 10 for i in range(400): 11 p=Process(target=work) # 在当前机器上,多进程耗时12.091996908187866s多,大部分时间耗费在创建进程上 12 # p=Thread(target=work) # 在当前机器上,多线程耗时2.050579309463501s 13 l.append(p) 14 p.start() 15 for p in l: 16 p.join() 17 stop=time.time() 18 print('run time is %s' %(stop-start))
由上述计算结果表明,一般情况下,只有当多核计算机在执行计算密集型操作时使用多进程,但是这不是一成不变的,要视具体情况而定,并且实际开发中我们都是多进程+多线程结合使用
多线程I/O操作时不会耗费时间
1 from threading import Thread 2 import time 3 4 n = 100 5 6 def task(): 7 global n 8 temp = n 9 time.sleep(1) 10 ''' 11 模拟I/O操作,当I/O进行时i,线程会暂时释放GIL,其他线程可以获得,他们都进行到这一步,n = 100 12 I/O操作结束,开始执行每个线程的n-1得到的都是99,所以结果是99 13 14 如果没有I/O操作,那因为GIL锁的原因,每个线程都需要执行完才会释放GIL, 15 故第二个线程获得的n是第一个线程-1后的结果,所以结果是0 16 ''' 17 n = temp - 1 18 19 l = [] 20 for i in range(100): 21 t = Thread(target=task) 22 t.start() 23 l.append(t) 24 for t in l: 25 t.join() 26 print(n)
time模拟I/O操作,当I/O进行时i,线程会暂时释放GIL,其他线程可以获得,他们都进行到这一步,n = 100I/O操作结束,开始执行每个线程的n-1得到的都是99,所以结果是99
如果没有I/O操作,那因为GIL锁的原因,每个线程都需要执行完才会释放GIL,故第二个线程获得的n是第一个线程-1后的结果,所以结果是0
2.死锁
什么是死锁
指的是两个或多个进程或线程在执行过程中,因争夺资源而造成了一种互相等待的问题,如果没有外力的作用,它们都无法执行下去,这时就是处于死锁状态
1 from threading import Thread,Lock 2 3 mutex = Lock() 4 5 def task(): 6 mutex.acquire() 7 print('hello world') 8 mutex.acquire() 9 ''' 10 想要获得这个锁,需要等到上面的人释放锁,但是他想要释放锁必须先获得锁,这就形成了死锁 11 ''' 12 print('hi') 13 mutex.release() 14 mutex.release() 15 16 t = Thread(target=task) 17 t.start()
线程1开始执行,time.sleep(1)模拟I/O操作,当第一个线程执行到这时,暂停了1秒,释放了GIL锁
线程2开始执行,当线程2抢到A锁,需要抢B锁,但是B锁在线程1中,这时线程1开始执行,需要故两个线程互相等待,形成了死锁
3.递归锁
什么是递归锁
RLock递归锁,可以被第一个抢到锁的人连续的acquire()和release()每次acquire,身上的计数加1,每次release身上的计数减1,只要身上的引用计数不为0,其他任何人都不能抢
1 from threading import Thread,RLock 2 3 mutex = RLock() 4 5 def task(): 6 mutex.acquire() 7 print('hello world') 8 mutex.acquire() 9 ''' 10 使用递归锁,可以避免形成死锁 11 ''' 12 print('hi') 13 mutex.release() 14 mutex.release() 15 16 t = Thread(target=task) 17 t.start()
1 from threading import Thread,Lock,RLock 2 import time 3 4 # mutexA = Lock() 5 # mutexB = Lock() 6 7 mutexA = RLock() 8 ''' 9 RLock递归锁,可以被第一个抢到锁的人连续的acquire()和release() 10 每次acquire,身上的计数加1,每次release身上的计数减1 11 只要身上的引用计数不为0,其他任何人都不能抢 12 ''' 13 14 class MyThread(Thread): 15 def run(self): 16 self.task() 17 self.task1() 18 19 def task(self): 20 mutexA.acquire() 21 print('%s抢到了A锁'%self.name) 22 mutexA.acquire() 23 print('%s抢到了B锁'%self.name) 24 mutexA.release() 25 print('%s释放了A锁' % self.name) 26 mutexA.release() 27 print('%s释放了B锁' % self.name) 28 29 def task1(self): 30 mutexA.acquire() 31 print('%s抢到了B锁' % self.name) 32 time.sleep(1) 33 mutexA.acquire() 34 print('%s抢到了A锁' % self.name) 35 mutexA.release() 36 print('%s释放了A锁' % self.name) 37 mutexA.release() 38 print('%s释放了B锁' % self.name) 39 40 for i in range(4): 41 t = MyThread() 42 t.start()
递归锁可以一定程度上解决死锁
4.信号量
一个互斥锁能控制只有抢到锁的线程在执行锁内的操作,
而信号量可以控制指定数量的线程执行锁内的操作
1 from threading import Thread,Semaphore 2 import time 3 4 s = Semaphore(3) # 允许最大3个线程同时执行 5 6 def task(i): 7 s.acquire() 8 print('%s正在执行'%i) 9 time.sleep(1) 10 s.release() 11 12 for i in range(10): 13 t = Thread(target=task,args=(i,)) 14 t.start()
5.event事件
event通过标志位来判断运行的状态
Event用法: event=threading.Event() #设置一个事件实例 event.set() #设置标志位 event.clear() #清空标志位 event.wait() #等待设置标志位 event.is_set #判断标志位是否被设置
1 from threading import Thread,Event 2 import time 3 import random 4 5 e = Event() # 生成一个event对象 6 7 def jump(): 8 print('飞机起飞,准备跳伞') 9 time.sleep(1.1) 10 print('倒计时5秒,机舱打开') 11 for i in range(6): 12 print(' %s'%(5-i),end= '') 13 time.sleep(1) 14 e.set() # 发送信号 15 print('') 16 print('机舱打开,开始跳伞') 17 18 19 def people(i): 20 time.sleep(random.random()) 21 print('伞兵%s号,准备完毕'%i) 22 e.wait() # 等待信号 23 time.sleep(random.random()) 24 print('伞兵%s号,开始跳伞'%i) 25 26 t = Thread(target=jump) 27 t.start() 28 29 for i in range(1,10): 30 t1 = Thread(target=people,args=(i,)) 31 t1.start()
6.队列补充
队列是管道+锁,使用队列就不需要自己手动操作锁的问题
因为锁操作的不好极容易产生死锁现象
队列: import queue # 导入模块 queue.Queue() # 生成队列对象,先进先出 queue.LifoQueue() # 生成堆栈对象,后进先出 queue.PriorityQueue() # 优先级队列,数字越小,优先级越高
1 from queue import Queue,LifoQueue,PriorityQueue 2 3 q = Queue() # 生成队列对象,先进先出 4 5 q.put(1) 6 q.put(2) 7 q.put(3) 8 print(q.get()) 9 print(q.get()) 10 print(q.get()) 11 12 q = LifoQueue() # 堆栈,后进先出 13 q.put(1) 14 q.put(2) 15 q.put(3) 16 print(q.get()) 17 print(q.get()) 18 print(q.get()) 19 20 q = PriorityQueue() # 优先级队列,根据传值的大小,小的先出队 21 # 传入元组,第一个值限制了取值时的优先级 22 q.put((1,1)) 23 q.put((1,5)) 24 q.put((1,3)) 25 print(q.get()) 26 print(q.get()) 27 print(q.get())
32