第三十八天 GIL 程序池與執行緒池
今日內容:
1.GIL 全域性直譯器鎖
2.Cpython直譯器併發效率驗證
3.執行緒互斥鎖和GIL對比
4.程序池與執行緒池
一.全域性直譯器鎖
1.GIL:全域性直譯器鎖
GIL本質就是一把互斥鎖,是夾在直譯器身上的
統一程序內的所有執行緒都需要先搶到GIL鎖,才能執行pai直譯器程式碼
2.GIL優缺點:
優點:
保證Cpython直譯器記憶體管理的執行緒安全
缺點:
同一程序內所有的執行緒同一時刻只能有一個執行,
也就是鎖Cpython直譯器多執行緒無法實現真正的並行
fromthreading import Thread,current_thread import time def task(): print("%s is running"%current_thread().name) time.sleep(3) print("%s is done"current_thread().name) if __name__=="__main__": t1=Thread(target=task) t2=Thread(target=task) t3=Thread(target=task) t1.start() t2.start() t3.start()
二.Cpython直譯器併發效率驗證
關於GIL效能的討論
直譯器加鎖以後
將導致所有執行緒只能併發 不能達到真正的並行 意味著同一時間只有一個CPU在處理你的執行緒
給你的感覺是效率低
程式碼執行有兩種狀態
阻塞 i/o 失去CPU的執行權 (CPU等待IO完成)
非阻塞 程式碼正常執行 比如迴圈一千萬次 中途CPU可能切換 很快會回來 (CPU在計算)
假如有32核CPU 要處理一個下載任務 網路速度慢 100k/s 檔案大小為1024kb
如果你的程式碼中IO操作非常多 cpu效能不能直接決定你的任務處理速度
案例:
目前有三個任務 每個任務處理需一秒 獲取元資料需要一小時
3個CPU 需要 一小時1秒
1個cpu 需要 一小時3秒
在IO密集的程式中 CPU效能無法直接決定程式的執行速度 python就應該幹這種活兒
在計算密集的程式中 CPU效能可以直接決定程式的執行速度
#計算密集型測試
from threading import Thread from multiprocessing import Process import time # 計算密集任務 def task1(): sum = 1 for i in range(10000000): sum *= i def task2(): sum = 1 for i in range(10000000): sum *= i def task3(): sum = 1 for i in range(10000000): sum *= i def task4(): sum = 1 for i in range(10000000): sum *= i def task5(): sum = 1 for i in range(10000000): sum *= i def task6(): sum = 1 for i in range(10000000): sum *= i if __name__ == '__main__': # 開始時間 st_time = time.time() # 多執行緒情況下 # t1 = Thread(target=task1) # t2 = Thread(target=task2) # t3 = Thread(target=task3) # t4 = Thread(target=task4) # t5 = Thread(target=task5) # t6 = Thread(target=task6) t1 = Process(target=task1) t2 = Process(target=task2) t3 = Process(target=task3) t4 = Process(target=task4) t5 = Process(target=task5) t6 = Process(target=task6) t1.start() t2.start() t3.start() t4.start() t5.start() t6.start() # # t1.join() # t2.join() # t3.join() # t4.join() # t5.join() # t6.join() print(time.time() - st_time)
from threading import Thread 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) def task5(): time.sleep(3) def task6(): time.sleep(3) if __name__ == '__main__': # 開始時間 st_time = time.time() # 多執行緒情況下 # t1 = Thread(target=task1) # t2 = Thread(target=task2) # t3 = Thread(target=task3) # t4 = Thread(target=task4) # t5 = Thread(target=task5) # t6 = Thread(target=task6) t1 = Process(target=task1) t2 = Process(target=task2) t3 = Process(target=task3) t4 = Process(target=task4) t5 = Process(target=task5) t6 = Process(target=task6) t1.start() t2.start() t3.start() t4.start() t5.start() t6.start() # t1.join() # t2.join() # t3.join() # t4.join() # t5.join() # t6.join() print(time.time() - st_time)
三.GIL與互斥鎖
from threading import Thread,Lock import time mutex = Lock() num = 1 def task(): global num # print(num) mutex.acquire() temp = num print(temp) time.sleep(1) # 當你們執行緒中出現io時 GIL鎖就解開 num = temp + 1 mutex.release() # 執行緒任務結束時GIL鎖解開 t1 = Thread(target=task,) t2 = Thread(target=task,) t1.start() t2.start() t1.join() t2.join() print(num)
GIL 和自定義互斥鎖的區別
全域性鎖不能保證自己開啟的執行緒安全 但要保證直譯器中的資料的安全
GIL 線上程呼叫直譯器是 自動加鎖 在IO阻塞時或執行緒執行完畢時 自動解鎖
四.程序池和執行緒池
程序池
就是一個裝程序的容器
為什麼出現
當程序很多的時候方便管理程序
什麼時候用?
當併發量特別大的時候 列入雙十一
很多時候程序是空閒的 就讓他進入程序池 讓有任務處理時才從程序取出來使用
程序池使用
ProcessPoolExecutor類
建立時指定最大程序數 自動建立程序
呼叫submit函式將任務提交程序池中
建立程序是在呼叫submit後發生
總結一下:
程序池可以自動創造程序
程序現在最大程序數
自動選擇一個空閒的程序幫你處理任務
程序什麼時候算是空閒?
程式碼執行完算是空閒
IO密集時 用執行緒池
計算密集時 用執行緒池
作業:
1、整理GIL直譯器鎖,解釋以下問題
1、什麼是GIL
GIL = Global Interpreter Lock (全域性解釋鎖器鎖)
2、有了GIL會對單程序下的多個執行緒造成什麼樣的影響
防止對個執行緒競爭統一資源造成資料的錯亂,只能有一個執行緒進行讀寫操作.
3、為什麼要有GIL
為了避免資源競爭造成的資料錯亂
4、GIL與自定義互斥鎖的區別,多個執行緒爭搶GIL與自定義互斥鎖的過程分析
GIL不保證自己開啟執行緒的安全 但保證直譯器中資料的安全
GIL 線上程呼叫解釋其時 自動加鎖 在IO阻塞是或執行緒程式碼執行完畢時,自動解鎖
5、什麼時候用python的多執行緒,什麼時候用多程序,為什麼?
2、程序池與執行緒池
1、池的用途,為何要用它
池用來儲存執行緒和程序,可以方便程序和執行緒的管理
2、池子裡什麼時候裝程序什麼時候裝執行緒?
計算密集時裝程序,IO密集時裝執行緒
3、基於程序池與執行緒池實現併發的套接字通訊
#服務端 from socket import * from threading import Thread from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor tpool=ThreadPoolExecutor(3) def communicte(conn,client_addr): while True: try: data= conn.recv(1024) if not data:break conn.send(data.upper()) except ConnectionAbortedError: break conn.close() def server(): server = socket(AF_INET,SOCK_STREAM) server.bind(('127.0.0.1',8080)) server.listen(5) while True: conn,client_addr=server.accept() print(client_addr) tpool.submit(communicte,conn,client_addr) server.close() if __name__=='__main__': server()
#客戶端 import socket c = socket.socket() c.connect(('127.0.0.1',8080)) while True: msg = input(">>>:") c.send(msg.encode("utf-8")) data = c.recv(1024) print(data.decode("utf-8"))
4、基於執行緒池實現一個可以支援併發通訊的套接字,完成以下功能?
執行客戶端程式,使用者可選的功能有:
1、登入
2、註冊
3、上傳
4、下載
思路解析:
1、執行登入,輸入使用者名稱egon,密碼123,對使用者名稱egon和密碼進行hash校驗,並加鹽處理,將密文密碼傳送到服務端,與服務端事先存好使用者名稱與密文密碼進行對比,對比成功後,
在服務端記憶體中用hash演算法生成一個隨機字串比如eadc05b6c5dda1f8772c4f4ca64db110
然後將該字串傳送給使用者以及登入成功的提示資訊傳送給客戶端,然後在服務存放好
current_users={
'a3sc05b6c5dda1f8313c4f4ca64db110':{'uid':0,'username':'alex'},
'e31adfc05b6c5dda1f8772c4f4ca64b0':{'uid':1,'username':'lxx'},
'eadc05b6c5dda1f8772c4f4ca64db110':{'uid':2,'username':'egon'},
}
使用者在收到服務端發來的'eadc05b6c5dda1f8772c4f4ca64db110'以及登入成功的提示資訊後,以後的任何操作都會攜帶該隨機字串'eadc05b6c5dda1f8772c4f4ca64db110‘,服務端會根據該字串獲取使用者資訊來進行與該使用者匹配的操作
在使用者關閉連線後,服務端會從current_users字典中清除使用者資訊,下次重新登入,會產生新的隨機字串
這樣做的好處:
1、使用者的敏感資訊全都存放到服務端,更加安全
2、每次登入都拿到一個新的隨機的字串,不容易被偽
2、執行註冊功能,提交到服務端,然後存放到檔案中,如果使用者已經存在則提示使用者已經註冊過,要求重新輸入使用者資訊
3、執行上次下載功能時會攜帶使用者的隨機字串到服務端,如果服務端發現該字串not in current_users,則要求使用者先登入