#一、GIL全局解释器锁 GIl是一个互斥锁:保证数据的安全(以牺牲效率来换取数据的安全) 阻止同一个进程内多个线程同时执行(不能并行但能实现并发) 并发:看起来像同时进行的) GIL全局解释器存在的原因是因为Cpython解释器的内存管理不是线程安全的 垃圾回收机制: (可以作为一种线程) 1、引用计数 2、标记清除 3、分带回收 同一个进程下的多个线程不能实现并行但是能够实现并发,多个进程下的线程能够实现并行 1、存在四个任务:计算密集型的任务 每个耗时10s 单核情况下:多线程好一点,消耗的资源较少 多核情况下: 开四个进程:10s多一些 一个进程下开启四个线程:40多秒 2、存在四个任务:IO密集型的任务 每个任务IO 10s 单核情况下:多线程好一些 多核情况下:多线程好一些
#计算密集与IO密集情况下线程与进程的耗时比较 #计算密集型 from multiprocessing import Process from threading import Thread import os,time def work(): res = 0 for i in range(12345678): res*=i if __name__ == '__main__': l = [] print(os.cpu_count()) #查看cpu核数 start_time = time.time() for i in range(4): #4个进程或者4个线程去计算 # p = Process(target=work) #run time is 3.224184513092041 p = Thread(target=work) #run time is 6.93839693069458 l.append(p) p.start() for p in l: p.join() stop_time = time.time() print('run time is %s'%(stop_time-start_time)) #IO密集型 from multiprocessing import Process from threading import Thread import time def work(): time.sleep(2) if __name__ == '__main__': l = [] start_time = time.time() for i in range(4): # p = Process(target=work) #2.416138172149658 p = Thread(target=work) #2.0021145343780518 l.append(p) p.start() for p in l: p.join() stop_time = time.time() print(stop_time-start_time) #结果验证正确
#二、GIL与普通锁的对比 #不加其他锁 from threading import Thread import time n = 100 def task(): global n tmp = n time.sleep(0.1) n = tmp - 1 t_list = [] for i in range(100): t = Thread(target=task) t.start() t_list.append(t) for t in t_list: t.join() print(n) #99 #解释:当启动线程后,由于GIL的存在,一次只能运行一个线程,在其拿到n=100的值后,遇到IO,进入阻塞态,这时 #系统强制结束该线程,换其他线程来执行,如此循环,导致所有线程拿到的都是n=100的值,最后一个线程结束后,返回的值是 # n=100-1,则为99。 这个结果没有达到我们的初衷,因此需要依靠其他锁来保证数据的安全 #加上其他锁 from threading import Thread,Lock import time n=100 mutex = Lock() def task(): global n mutex.acquire() tmp = n time.sleep(0.1) n = tmp - 1 mutex.release() t_list = [] for i in range(100): t = Thread(target=task) t_list.append(t) t.start() for t in t_list: t.join() print(n) #0 #解释:加上了锁以后,第一个线程先拿到了锁,拿到了n=100这个数据,然后遇到了IO阻塞,系统强制结束该进程,其他线程开始运行,但是 #发现数据被加锁了,无法进一步运行,到时间了,系统又让其强制结束,如此往复,直到第一个进程再次拿到了执行权限,此时的阻塞已经过去, # 该进程进行相关运算,再释放锁,交给其他线程去竞争,如此循环往复,最后得到n = 0 因此对于不同的数据,要想保证安全,需要加上不同的锁去处理 GIL并不能保证数据的安全,他是对Cpython解释器加锁,针对的是线程 保证的是在同一个进程中多个线程一个时间内只能运行一个线程
#三、死锁与递归锁 #死锁 from threading import Thread,Lock import time mutex1 = Lock() mutex2 = Lock() class MyThead(Thread): def run(self): self.fun1() self.fun2() def fun1(self): mutex1.acquire() print('%s 抢到A锁'%self.name) mutex2.acquire() print('%s 抢到了B锁'%self.name) mutex2.release() print('%s释放了B锁'%self.name) mutex1.release() print('%s 释放了A锁'%self.name) def fun2(self): mutex2.acquire() print('%s抢到了B锁'%self.name) time.sleep(1) mutex1.acquire() print('%s 抢到了A锁'%self.name) mutex1.release() print('%s 释放了A锁'%self.name) mutex2.release() print('%s 释放了B锁'%self.name) for i in range(100): t = MyThead() t.start() #结果: Thread-1 抢到A锁 Thread-1 抢到了B锁 Thread-1释放了B锁 Thread-1 释放了A锁 Thread-1抢到了B锁 Thread-2 抢到A锁 然后程序就会卡着,此时就陷入了死锁 因为线程1拿到了B锁,线程2拿到了A锁,此时彼此拿着对方的命脉,不给对方活路,除非有外力,不然就会一直这样 #递归锁 from threading import RLock,Thread import time mutexA = mutexB =RLock() class MyThead(Thread): def run(self): self.fun1() self.fun2() def fun1(self): mutexA.acquire() print('%s抢到了A锁'%self.name) mutexB.acquire() print('%s抢到了B锁'%self.name) mutexB.release() print('%s释放了B锁'%self.name) mutexA.release() print('%s释放了A锁'%self.name) def fun2(self): mutexB.acquire() print('%s抢到了B锁'%self.name) time.sleep(1) mutexA.acquire() print('%s抢到了A锁'%self.name) mutexA.release() print('%s释放了A锁'%self.name) mutexB.release() print('%s释放了B锁'%self.name) for i in range(10): t = MyThead() t.start() #总结 自定义锁一次acquire必须对应一次release,不能连续acquire 递归锁Rlock:这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。 直到一个线程所有的acquire都被release,其他的线程才能获得资源。 mutexA=mutexB=threading.RLock() #一个线程拿到锁,counter加1,该线程内又碰到加锁的情况,则counter继续加1, 这期间所有其他线程都只能等待,等待该线程释放所有锁,即counter递减到0为止
#四、信号量 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,5)) #模拟蹲坑耗时 sm.release() for i in range(20): t = Thread(target=task,args=('巴豆%s号'%i,)) t.start() #和普通的互斥锁区别在于,普通的是独立卫生间,所有人抢一个坑位 #信号量 是公共卫生间,有多个茅坑,所有人抢多个坑位
#五、线程 queue 使用import queue ,用法与进程的Queue一样 #先进先出 queue.Queue #先进后出 queue.LifoQueue #优先级 queue.PriorityQueue import queue q = queue.Queue(3) #队列里最多放置的数据个数 q.put(1) q.put(2) print(q.get()) #1 print(q.get()) #2 q = queue.LifoQueue(3) q.put(1) q.put(2) print(q.get()) #2 print(q.get()) #1 q = queue.PriorityQueue(3) q.put((10,'a')) q.put((-1,'a')) q.put((0,'a')) print(q.get()) #(-1, 'a') print(q.get()) #(0, 'a') print(q.get()) #(10, 'a') #对于优先级,元组里的第一个元素通常是数字,也可以是非数字之间去比较大小 #比较的结果中,该元素越小,优先级越高
#六、event事件 线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果程序中的其 他线程需要通过判断某个线程的状态来确定自己下一步 的操作,这时线程同步问题就会变得非常棘手。为了解决这些问题,我们需要使用threading库中的Event对象。 对象包含一个可由线程 设置的信号标志,它允许线程等待某些事件的发生。在 初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象 , 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真, 它将唤醒所有等待这个Event对象的线程。 如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行 event.isSet():返回event的状态值; event.wait():如果 event.isSet()==False将阻塞线程; event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度; event.clear():恢复event的状态值为False。 #多线程尝试连接MySQL from threading import Thread,Event import threading import time,random # event = Event() def conn_mysql(): count = 1 while not event.is_set(): if count > 3: raise TimeoutError('链接超时') print('<%s>第%s尝试链接'%(threading.current_thread().getName(),count)) event.wait(0.5) count += 1 print('<%s>链接成功'%threading.current_thread().getName()) def check_mysql(): print(' 33[45m[%s]正在检查mysql 33[0m'%threading.current_thread().getName()) time.sleep(random.random()) event.set() event = Event() conn1 = Thread(target=conn_mysql) conn2 = Thread(target=conn_mysql) check = Thread(target=check_mysql) conn1.start() conn2.start() check.start()