標籤:訪問 看不見 運行 保護 python程式 程式 多資源 修改 wiki
一、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,Lockimport timemutex=Lock()n=100def task(): global n with mutex: temp=n time.sleep(0.1) n=temp-1if __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)
test
通過自訂互斥鎖,每個線程除了要搶到GIL鎖之外還要搶到自訂的鎖,否則即使搶到了GIL也沒有用,這就充分保證了資料的安全性。
三、GIL與多線程
既然有了GIL的存在,一個進程中同一時刻只有一個線程能夠被執行,無法利用cpu的多核機制,和多進程一比,是不是多進程反而更佔優勢了呢。
那多核機制有什麼好處呢?
cpu是用來做計算的,多核,意味多個cpu去完成計算功能,提升計算效能,但是cpu一旦遇到 I/O操作,那麼多核對I/O就沒有什麼協助了。
#計算操作from multiprocessing import Processimport os,timefrom threading import Threaddef work(): res=0 for i in range(100000000): res+=iif __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 Threadfrom multiprocessing import Processimport timedef 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,Lockimport timemutexA=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,RLockimport timemutexB=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,Semaphoreimport time,randomsm=Semaphore(5)#最大串連數為5def 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,Eventimport timeevent=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())#3print(q.get())#2print(q.get())#1
queue.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、死結遞迴鎖及線程補充