0x00 前言
本片文章讲述了小明同学在编写python多线程过程中遇到一些奇怪现象,小明根据这些奇怪现象挖掘背后的原因...通过遇到的问题,引申出全局解释器锁,同步锁,递归锁,信号量...
0x01 全局解释器锁
小明同学在实验过程中,需要计算一个加法和一个乘法,觉得单线程运行时间较长,所以改为多线程,不料发现线程比单线程运行时间还长...
单线程代码如下,运行时间为8.41097640991211
import timestart_time=time.time()def add(): q=0 for q_i in range(100000000): q=q+q_idef mcl(): w=0 for w_i in range(100000000): w=w*w_iadd()mcl()end_time=time.time()print(end_time-start_time)
多线程代码如下,运行时间为8.47524094581604
import timeimport threadingstart_time=time.time()def add(): q=0 for q_i in range(100000000): q=q+q_idef mcl(): w=0 for w_i in range(100000000): w=w*w_it1=threading.Thread(target=add,args=())t2=threading.Thread(target=mcl,args=())t1.start()t2.start()t1.join()t2.join()end_time=time.time()print(end_time-start_time)
多线程为什么没有起作用,猜测程序执行过程是串行的,这就是全局解释器锁导致的,全局解释器锁的概念如下:
在同一个进程中只要有一个线程获取了全局解释器(cpu)的使用权限,那么其他的线程就必须等待该线程的全局解释器(cpu)使用权消失后才能使用全局解释器(cpu),即时多个线程直接不会相互影响在同一个进程下也只有一个线程使用cpu,这样的机制称为全局解释器锁(GIL)。
最后通过修改为多进程可以节省运行时间,5.125233888626099
import timefrom multiprocessing import Processstart_time=time.time()def add(): q=0 for q_i in range(100000000): q=q+q_idef mcl(): w=0 for w_i in range(100000000): w=w*w_iif __name__ == '__main__': p1 = Process(target=add, args=()) p2 = Process(target=mcl, args=()) p1.start() p2.start() p1.join() p2.join() end_time=time.time() print(end_time-start_time)
0x02 同步锁
小明同学在实验过程中,需要用多线程计算一个减法,将数字100减到0,小明同学开了100个线程,每个线程减1,最后应该减为0...最后却没有达到期望结果。
小明写的代码如下,计算结果却是94。
import timeimport threadingnumber=100def sub(): global number tmp_num=number time.sleep(0.0005) number=tmp_num-1thread_list=[]for i in range(100): t=threading.Thread(target=sub,args=()) t.start() thread_list.append(t)for thread_i in thread_list: thread_i.join()print(number)
小明请教老师帮忙需寻找到的原因为,cpu第一个线程执行到sub函数的tmp_num=number步骤,未完成执行情况下,因为存在IO阻塞(time.sleep) ,CPU会切换到第二个线程依旧执行到sub函数的tmp_num=number步骤,此时number也还是100,假设线程二执行完毕此时number为99,CPU再切回线程一继续执行之前上下文计算出来number也是99,赋值给number为99,可以看出执行完2个线程了实际number只减一。经过指导小明给线程加上线程锁。运算结果为0
import timeimport threadingnumber=100syn_lock=threading.Lock()def sub(): global number syn_lock.acquire() tmp_num=number time.sleep(0.0005) number=tmp_num-1 syn_lock.release()thread_list=[]for i in range(100): t=threading.Thread(target=sub,args=()) t.start() thread_list.append(t)for thread_i in thread_list: thread_i.join()print(number)
综上总结:
1、什么是同步锁?
同一时刻的一个进程下的一个线程只能使用一个cpu,要确保这个线程下的程序在一段时间内被cpu执,那么就要用到同步锁。
2、为什么用同步锁?
因为有可能当一个线程在使用cpu时,该线程下的程序可能会遇到io操作,那么cpu就会切到别的线程上去,这样就有可能会影响到该程序结果的完整性。
3、怎么使用同步锁?
只需要在对公共数据的操作前后加上上锁和释放锁的操作即可。
0x03 死锁
因为疫情严重,学校假期有个规定,每次只准一个人进入学校,学校有两把锁,进入学校后需要先用A锁上学校门,进入教室在C锁上教室门。小明先用锁A,然后用锁C,成功进入教室,此时两把锁都是用完了处于暂时释放状态。然后拿完东西想出学校,小明此时使用C锁走出教室。于此同时,刚才小白看两把锁都是处于暂时释放状态,也想进入学校,便使用了锁A;
然后问题出现了,小明此时占用锁C想使用锁A,小白此时占用锁A想使用锁C。这种情况就造成了死锁。
死锁:两个或两个以上的线程或进程在执行程序的过程中,因争夺资源而相互等待的一个现象。
import timeimport threadingthread_lock_1=threading.Lock()thread_lock_2=threading.Lock()def move_school(): thread_lock_1.acquire() print("%s进入学校门使用了thread_lock_1"%threading.current_thread()) time.sleep(3) thread_lock_2.acquire() print("%s进入教室门thread_lock_2"%threading.current_thread()) time.sleep(2) thread_lock_2.release() thread_lock_1.release()def move_home(): thread_lock_2.acquire() print("%s出去教室门thread_lock_2"%threading.current_thread()) time.sleep(2) thread_lock_1.acquire() print("%s出去学校门thread_lock_1"%threading.current_thread()) time.sleep(3) thread_lock_1.release() thread_lock_2.release()def game(): move_school() move_home()thread_list=[]for i in range(0,2): t=threading.Thread(target=game) t.start() thread_list.append(t)for t in thread_list: t.join()print("-----------end----------")
打印结果如下:
进入学校门使用了thread_lock_1进入教室门thread_lock_2出去教室门thread_lock_2进入学校门使用了thread_lock_1........卡住
0x04 递归锁
死锁的原因是两个或两个以上的线程或进程在执行程序的过程中,因争夺资源而相互等待的一个现象。
那么我们在多线程中同时让一个线程只能使用一把锁,问题得以解决。但是这个锁特殊之处在于锁的内部还可以生成锁。所以这里称为递归锁。
递归锁:
在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。
二者的区别是:
递归锁可以连续acquire多次,每acquire一次计数器加1,只要计数不为0,就不能被其他线程抢到。只有计数为0时,才能被其他线程抢到acquire。释放一次计数器-1,而互斥锁只能加锁acquire一次,想要再加锁acquire,就需要release解之前的锁。
import timeimport threadingthread_lock_1=threading.RLock()def move_school(): thread_lock_1.acquire() print("%s进入学校门使用了thread_lock_1"%threading.current_thread()) time.sleep(3) thread_lock_1.acquire() print("%s进入教室门thread_lock_2"%threading.current_thread()) time.sleep(2) thread_lock_1.release() thread_lock_1.release()def move_home(): thread_lock_1.acquire() print("%s出去教室门thread_lock_2"%threading.current_thread()) time.sleep(2) thread_lock_1.acquire() print("%s出去学校门thread_lock_1"%threading.current_thread()) time.sleep(3) thread_lock_1.release() thread_lock_1.release()def game(): move_school() move_home()thread_list=[]for i in range(0,2): t=threading.Thread(target=game) t.start() thread_list.append(t)for t in thread_list: t.join()print("-----------end----------")
执行结果如下
D:\py_project\venv\Scripts\python.exe D:/py_project/递归锁.py<Thread(Thread-1, started 16908)>进入学校门使用了thread_lock_1<Thread(Thread-1, started 16908)>进入教室门thread_lock_2<Thread(Thread-2, started 6292)>进入学校门使用了thread_lock_1<Thread(Thread-2, started 6292)>进入教室门thread_lock_2<Thread(Thread-1, started 16908)>出去教室门thread_lock_2<Thread(Thread-1, started 16908)>出去学校门thread_lock_1<Thread(Thread-2, started 6292)>出去教室门thread_lock_2<Thread(Thread-2, started 6292)>出去学校门thread_lock_1-----------end----------Process finished with exit code 0
0x05 信号量
假设停车场一次可以停五辆车,我们可以将并发调整为五threading.Semaphore(5),如果不适用信号量的话,1秒种会将50辆车全部停进去了。
Semaphore管理一个内置的计数器,每当调用acquire()时内置计数器-1;调用release() 时内置计数器+1;计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。
实例:(同时只有5个线程可以获得semaphore,即可以限制最大连接数为5):
import timeimport threadingsemaphore=threading.Semaphore(5)def stop_car(n): semaphore.acquire() time.sleep(1) print("这是第%s车子"%n) semaphore.release()thread_list=[]for x in range(0,50): t=threading.Thread(target=stop_car,args=(x,)) t.start() thread_list.append(t)for t in thread_list: t.join()print("----------------end-------------")
打印结果会是5辆车一起打印出来。