Python課程回顧(day31)
GIL(全域性直譯器鎖)
什麼是全域性直譯器鎖?
根據我們之前學到過的互斥鎖,我們知道了鎖在我們程序與執行緒中的作用就是為了讓程序與執行緒在讀寫同一份資料時不會發生混亂的,那麼全域性直譯器鎖本質上也就是一把互斥鎖,但全域性直譯器鎖只在Cpython直譯器中有。那麼全域性直譯器鎖又有何作用?
首先我們來回顧一下python程式執行的三個步驟:
⑴啟動python直譯器,申請記憶體空間將直譯器的程式碼放進去
⑵將python檔案由硬碟載入到剛剛已經申請過的記憶體空間
⑶再由python直譯器讀取python檔案解釋執行程式碼
全域性直譯器的作用
之前我們學到過程序與執行緒的理論知識,所以我們知道每個程序都含有一條執行任務的主執行緒,而主執行緒在執行程式碼時會分支出多個子執行緒,由於我們的python直譯器與我們的python檔案此時都在同一記憶體空間內,所以多個子執行緒都會去爭搶python直譯器來解釋執行程式碼,若不加以管理則會有可能導致多個執行緒併發執行的去修改程序內的資料,這就是程序安全問題。但直譯器只有一個,所以全域性直譯器鎖的作用就是在一個程序內多個執行緒只能有一個執行,即每個要執行程式碼的執行緒都要先搶到這把加在直譯器身上的鎖才可以執行。保證了Cpython直譯器記憶體管理的安全性。
當然,GIL全域性直譯器鎖的缺點也是顯而易見的,它也導致在同一執行緒內的所有執行緒同一時刻只能有一個執行緒執行,會導致Cpython直譯器的多執行緒無法實現並行效果,在遇到計算密集型別時的操作則會影響執行效率,Cpython的解決方案則是在多核cpu的前提下再開一個程序來實現一個並行的效果,而單核cpu時無論多程序還是多執行緒本質上的區別是不大的。在遇到IO密集型別的多個任務時,使用多執行緒則會比多程序更加節省cpu資源,不會佔用多個cpu。
Cpython直譯器併發效率驗證
首先我們看一下多程序在進行多個運算時的時間
使用多程序同時計算多個數據:
from multiprocessing import Process import time def task1(): res = 0 for i in range(1, 100000000): res += i def task2(): res = 0 for i in range(1, 100000000): res += i def task3(): res = 0 for i in range(1, 100000000): res += i def task4(): res = 0 for i in range(1, 100000000): res += i if __name__ == '__main__': s = time.time() p1 = Process(target=task1) p2 = Process(target=task2) p3 = Process(target=task3) p4 = Process(target=task4) p1.start() p2.start() p3.start() p4.start() p1.join() p2.join() p3.join() p4.join() print(time.time() - s)
輸出結果為:17.96002721786499
使用多執行緒計算多個數據:
from threading import Thread import time def task1(): res = 0 for i in range(1, 100000000): res += i def task2(): res = 0 for i in range(1, 100000000): res += i def task3(): res = 0 for i in range(1, 100000000): res += i def task4(): res = 0 for i in range(1, 100000000): res += i if __name__ == '__main__': s = time.time() t1 = Thread(target=task1) t2 = Thread(target=task2) t3 = Thread(target=task3) t4 = Thread(target=task4) t1.start() t2.start() t3.start() t4.start() t1.join() t2.join() t3.join() t4.join() print(time.time() - s)
輸出結果為:31.936826705932617
目前我們可以看出在計算型別操作時多程序的並行效果要遠超與多執行緒的依次運算的,下面我們來看一下IO型別操作的速度。
使用多程序並行執行IO型別操作:
from multiprocessing import Process import time def task1(): time.sleep(3) def task2(): time.sleep(3) def task3(): time.sleep(3) def task4(): time.sleep(3) if __name__ == '__main__': s = time.time() p1 = Process(target=task1) p2 = Process(target=task2) p3 = Process(target=task3) p4 = Process(target=task4) p1.start() p2.start() p3.start() p4.start() p1.join() p2.join() p3.join() p4.join() print(time.time() - s) 輸出結果為:3.1831820011138916
使用多執行緒執行IO型別操作:
from threading import Thread import time def task1(): time.sleep(3) def task2(): time.sleep(3) def task3(): time.sleep(3) def task4(): time.sleep(3) if __name__ == '__main__': s = time.time() t1 = Thread(target=task1) t2 = Thread(target=task2) t3 = Thread(target=task3) t4 = Thread(target=task4) t1.start() t2.start() t3.start() t4.start() t1.join() t2.join() t3.join() t4.join() print(time.time() - s)
輸出結果為:3.001171588897705
結論:通過兩次驗證證明多執行緒在執行IO型別操作時效率看起來只比多程序快一點點,因為多程序的開銷與開啟時間都比較大,但兩者之間是差著數量級的,算起來也是多程序的百倍左右。而多程序在計算型別操作時效率也是要遠超多執行緒,但事實是我們編寫的程式例如之前的ATM購物車,選課系統等多數的操作都是要與使用者互動資料操作,而這些操作就是一些大量的IO操作,真正意義上的純運算程式其實特別少,所以我們以後編寫程式時也會遇到大量的IO操作,假設真的有純運算的程式,我們也確實應該使用多程序。
GIL與互斥鎖的關聯
首先我們要明白,雖然外表上看起來GIL與互斥鎖的作用是相等的,但GIL的作用只是保護直譯器級別的資料在同一時間只能有一個執行緒執行,若該執行緒在執行程式碼時遇到了IO作業系統則會回收直譯器鎖以供下面的程序搶鎖,所以這並不能保證執行緒自己執行的程式碼的安全性。
GIL配合互斥鎖
from threading import Thread, Lock import time mutex = Lock() count = 0 def task(): global count mutex.acquire() # 後來拿到互斥鎖的執行緒會卡在此處直到第一個拿到鎖的人離開鎖 temp = count time.sleep(0.1) # 執行緒在拿到GIL直譯器鎖之後執行了IO操作,所以直譯器鎖會將鎖回收 # 下個程序搶到鎖後也會執行到此處進行IO操作,依次迴圈 # 第一個先睡完的執行緒會再次搶GIL直譯器鎖 # 拿到鎖後更改資料 count = temp + 1 mutex.release() if __name__ == '__main__': t_l = [] for i in range(2): t = Thread(target=task) t_l.append(t) # 每建立一個執行緒物件都新增到列表內,節省程式碼 t.start() for t in t_l: # 從列表內依次join t.join() print('主', count)
程序池與執行緒池
在瞭解了併發程式設計之後,我們可以使用執行緒與程序不間斷的為使用者提供服務,但是我們還要思考一個問題,那就是我們的伺服器即便再強大也是有負載上限的,你可以執行一千個一萬個程序,但幾百萬幾千萬呢?很顯然在面對未知數的客戶端時我們並不能保證我們的伺服器永遠也不會崩潰。所以我們就要想到一種辦法來解決這個問題了。就是程序池與執行緒池!
1.什麼是程序池?什麼是執行緒池?
首先我們知道,池的意思就是用來存放東西的,例如我們的水池,花池等等。那顧名思義程序池與執行緒池就是用來存放程序與執行緒的一種容器。而程序池與執行緒池在我們python中就是限制我們計算機併發執行的任務數量,使我們的計算機在一個自己可承受的範圍內併發的去執行任務。
2.關鍵字:from concurrent.futures import ProcessPoolExecutor/ThreadPoolExecutor
使用執行緒池完成併發套接字通訊
服務端程式碼: from socket import * # 匯入執行緒池模組 from concurrent.futures import ThreadPoolExecutor # 建立一個執行緒池物件, 括號內的max_worker指的是最大執行緒數 # 預設為cpu的核數乘5, 也可指定相應數字, 預設乘5 # 執行緒數若超出上限則會等待執行緒池內有執行緒空閒下來再執行給執行緒繫結的功能 T_pool = ThreadPoolExecutor(max_workers=4) # 執行緒執行的功能 def task(conn): while True: try: data = conn.recv(1024) if not data: break conn.send(data.upper()) except ConnectionResetError: break conn.close() def servers(): # 建立伺服器物件並配置資訊 server = socket(AF_INET, SOCK_STREAM) server.bind(('127.0.0.1', 9898)) server.listen(5) # 為套接字通訊加上鍊接迴圈 while True: conn, client_address = server.accept() print(client_address) # 使用關鍵字submit傳送建立執行緒訊號並傳入相應的引數 # 傳送完訊號後再迴圈等待接收連結請求 T_pool.submit(task, conn) if __name__ == '__main__': servers()
客戶端程式碼: from socket import * client = socket() client.connect(('127.0.0.1', 9898)) while True: msg = input('>>:') if not msg: continue if msg == 'q': break client.send(msg.encode('utf-8')) data = client.recv(1024) print(data.decode('utf-8')) client.close()
程序池除了名稱不同其他屬性都與執行緒池的屬性是一樣的,這裡不使用程序池的原因是因為我們所寫的套接字通訊大都是IO操作,基本沒有運算任務,所以此處使用執行緒則會提高程式的執行效率。
強調:
一定要清楚的知道程序與執行緒的最大區別與什麼時候使用程序與執行緒。
程序池要放入的都是運算密集型別的程序任務
執行緒池要放入的都是IO密集型別的執行緒任務