python-GIL、死鎖遞歸鎖及線程補充
一、GIL介紹
GIL全稱 Global Interpreter Lock ,中文解釋為全局解釋器鎖。它並不是Python的特性,而是在實現python的主流Cpython解釋器時所引入的一個概念,CIL本質上就是一把互斥鎖,將並發運行變成串行,以此來控制同一時間內共享數據只能被一個任務所修改,從而保證數據的安全性。
註:每次執行python程序,都會產生一個獨立的進程,進程裏除了能看到的若幹線程,還有看不見的解釋器開啟的垃圾回收等解釋器級別的線程。
#1 所有數據都是共享的,這其中,代碼作為一種數據也是被所有線程共享的(test.py的所有代碼以及Cpython解釋器的所有代碼) 例如:test.py定義一個函數work(代碼內容如下圖),在進程內所有線程都能訪問到work的代碼,於是我們可以開啟四個線程然後target都指向該代碼,能訪問到意味著就是可以執行。#2 所有線程的任務,都需要將任務的代碼當做參數傳給解釋器的代碼去執行,即所有的線程要想運行自己的任務,首先需要解決的是能夠訪問到解釋器的代碼。
多個線程先訪問到解釋器的代碼,去拿去執行權限,然後將自己target的代碼拿給解釋器去執行,解釋器的代碼是對所有線程都共享的,這個時候就存在一個問題,垃圾回收線程也可以去訪問解釋器代碼,對於同一個數據,可能線程1去修改它的數據的同時 垃圾回收對他執行的是回收操作,這個時候就會導致很多無法預料的bug。GIL加鎖處理,就是保證解釋器同一時間內只能執行一個任務的代碼。這就導致了同一個進程下的線程無法實現並行,不能很好的利用cpu的多核機制,但是還是可以實現並行的。(如果想實現並行只能開啟多個進程)
二、GIL與Lock的區別
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): ttest=Thread(target=task) l.append(t) t.start() for t in l: t.join() print(n)
通過自定義互斥鎖,每個線程除了要搶到GIL鎖之外還要搶到自定義的鎖,否則即使搶到了GIL也沒有用,這就充分保證了數據的安全性。
三、GIL與多線程
既然有了GIL的存在,一個進程中同一時刻只有一個線程能夠被執行,無法利用cpu的多核機制,和多進程一比,是不是多進程反而更占優勢了呢。
那多核機制有什麽好處呢?
cpu是用來做計算的,多核,意味多個cpu去完成計算功能,提升計算性能,但是cpu一旦遇到 I/O操作,那麽多核對I/O就沒有什麽幫助了。
#計算操作 from multiprocessing import Process import os,time from threading import Thread def work(): res=0 for i in range(100000000): res+=i if __name__ == ‘__main__‘: print(os.cpu_count()) p_l =[] start = time.time() for i in range(4): p = Process(target=work)#5.057471036911011 # p = Thread(target=work)#18.38089609146118 p.start() p_l.append(p) for j in p_l: j.join() print(‘time is %s‘%(time.time()-start))計算操作
#io操作 from threading import Thread from multiprocessing import Process import time def work(): time.sleep(4) if __name__ == ‘__main__‘: p_l = [] start = time.time() for i in range(4): p = Process(target=work)#4.281008243560791 # p = Thread(target=work)#4.002274751663208 p_l.append(p) p.start() for j in p_l: j.join() print(p_l) print(‘time is %s‘ % (time.time() - start))I/O操作
總結:
多線程用於I/O密集型,如socket,爬蟲,web等
多進程用於計算密集型,如金融分析等。
四、死鎖與遞歸鎖
死鎖:兩個或兩個以上的進程或者線程在執行過程中,因為真多資源二造成的互相等待現象,若無外力的作用,題目都將一直處於阻塞狀態,這些互相等待的進程或者線程就被稱為死鎖。
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鎖 然後就卡住,死鎖了 ‘‘‘test
解決方法,使用遞歸鎖(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()test
五、信號量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()test
六、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()test
七、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())#1queue.LifoQueue
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‘)PriorityQueue
python-GIL、死鎖遞歸鎖及線程補充