五、GIL vs 互斥鎖(*****) 一、什麼是GIL(Global Interpreter Lock) GIL是全局解釋器鎖,是加到解釋器身上的,保護的就是解釋器級別的數據 (好比垃圾回收的數據) 同一個進程內的全部線程都須要先搶到GIL鎖,才能執行解釋器代碼 2 爲何須要GIL python 中內存管理依賴於 GC(一段用於回收內存的代碼) 也須要一個線程 除了你本身開的線程 系統還有一些內置線程 就算你的代碼不會去競爭解釋器 內置線程也可能會競爭 因此必須加上鎖 三、GIL的影響 GIl會限制同一進程的內的多個線程同一時間只能有一個運行,也就是說python一個進程內的多線線程 沒法實現並行的效果,即沒法利用多核優點 而後多核提供的優點是同一時刻有多個cpu參與計算,意味着計算性能地提高,也就是說咱們的任務是 計算密集型的狀況下才須要考慮利用多核優點,此時應該開啓python的多進程 在咱們的任務是IO密集型的狀況下,再多的cpu對性能的提高也用處不大,也就說多核優點在IO密集型程序面前 發揮的做用微乎其微,此時用python的多線程也是能夠的 GIL的優缺點: 優勢: 保證Cpython解釋器內存管理的線程安全 缺點: 同一進程內全部的線程同一時刻只能有一個執行,沒法利用多核CPU 也就說Cpython解釋器的多線程沒法實現並行 (問題: 一個py程序 要想運行 必須運行解釋器 解釋器的工做時翻譯代碼 並執行 當一個py進程中 有多個線程 線程的任務就是執行代碼 意味者 多個線程都要使用解釋器 簡單的說 多線程會爭搶解釋器的執行權 若是是本身開的線程 多線程要訪問相同數據 加鎖就能解決 可是有一寫代碼不是程序員寫的 也確實須要共享使用 就是解釋器 GC:垃圾回收器 負責清理內存中的無用數據 清理垃圾也須要執行代碼 可是GC不該該卡住用戶的代碼執行 只能開線程 GC 看到 x = 10 x = 1 準備刪除10 這時候忽然CPU切到用戶線程 a = 10 此此時尚未問題 緊接着 CPU 又切到GC GC上來就刪除10 在切到用戶線程 a 所指向的地址被清理了 產生錯誤 解決方案: 給解釋器加上鎖 保證GC執行期間 用戶線程不能執行) 四、GIL vs 自定義鎖 保護不一樣的數據就應該加不一樣的鎖。 相同點:都是互斥鎖 不一樣點: GIL解釋器級別鎖 鎖的是解釋器代碼 自定義鎖 鎖的是本身寫的代碼 GIL 在當一個線程調用解釋器時 自動加鎖 在IO阻塞時或線程代碼執行完畢/執行時間過長3ms時 自動解鎖 本質就是一個互斥鎖,而後保護不一樣的數據就應該用不一樣的互斥鎖,保護咱們應用程序級別的數據必須自定義互斥鎖 有了GIL 爲何還須要自定義鎖? GIL 不清楚什麼代碼會形成數據競爭問題 不知道什麼地方該加 6 Cpython的解釋器下,多線程是雞肋?***** 多個任務是IO密集型:多線程 (IO的速度 明顯要比CPU執行速度慢) 多個任務是計算密集型:多進程 七、死鎖現象與遞歸鎖(可重入鎖),信號量(**) 死鎖? 進程也有死鎖與遞歸鎖,在進程那裏忘記說了,放到這裏一切說了額 所謂死鎖: 是指兩個或兩個以上的進程或線程在執行過程當中,因爭奪資源而形成的一種互相等待的現象, 若無外力做用,它們都將沒法推動下去。此時稱系統處於死鎖狀態或系統產生了死鎖, 這些永遠在互相等待的進程稱爲死鎖進程,以下就是死鎖 解決方法,遞歸鎖,在Python中爲了支持在同一線程中屢次請求同一資源,python提供了可重入鎖RLock。 這個RLock內部維護着一個Lock和一個counter變量,counter記錄了acquire的次數, 從而使得資源能夠被屢次require。直到一個線程全部的acquire都被release, 其餘的線程才能得到資源。上面的例子若是使用RLock代替Lock,則不會發生死鎖: mutexA=mutexB=threading.RLock() #一個線程拿到鎖,counter加1,該線程內又碰到加鎖的狀況, 則counter繼續加1,這期間全部其餘線程都只能等待,等待該線程釋放全部鎖,即counter遞減到0爲止 死鎖形成的問題.程序卡死 一個鎖不會產生死鎖 當有多個鎖多個線程時會產生死鎖 a b 鎖 p k 線程 當p 和 k 都須要a和b鎖時纔可能產生死鎖 遞歸鎖(可重入鎖)RLock 同一個線程能夠屢次執行acquire 執行一次acquire 計數加1 執行一次release 次數減一 執行acquire的次數須要與release的次數對應 在執行被鎖的代碼時 同一個線程 不會判斷次數 其餘線程須要判斷 計數爲0才能夠執行 不是用來解決死鎖的 Semaphore信號量(瞭解) 經常使用在線程中 信號量做用:限制同時執行被鎖代碼的線程數量 案列: sem = semaphore(2) acquire code..... release 開了十個線程 只能有兩個同時執行 八、隊列queue(***) queue 這個queue和進程裏的Queue不一樣 就是一個簡單的容器 隊列是一種數據的容器 特色:先進先出 queue先進先出 lifoqueue先進後出 priorityqueue 優先級隊列 整型表示優先級 數字越大優先級越低 import queue q = queue.Queue()# 普通隊列 先進先出 q.put("a") q2 = queue.LifoQueue()# 堆棧隊列 先進後出 後進先出 函數調用就是進棧 函數結束就出棧 遞歸形成棧溢出 q3 = queue.PriorityQueue() # 優先級隊列 九、Event事件(**)瞭解 python線程的事件用於主線程控制其餘線程的執行,事件主要提供了三個方法 set、wait、clear。 事件處理的機制:全局定義了一個「Flag」,若是「Flag」值爲 False, 那麼當程序執行 event.wait 方法時就會阻塞,若是「Flag」值爲True,那麼event.wait 方法時便再也不阻塞。 是什麼? 線程間通信的方式 爲何用? 簡化代碼 set()設置爲True wati()阻塞 直到爲True clear:將「Flag」設置爲False四、池(*****) 就是一個裝進程/線程的容器 爲什麼要用池: 操做系統沒法無限開啓進程或線程 池做用是將進程或線程控制操做系統可承受的範圍內 何時用進程池: (好比 雙十一) 當程序中有多個進程時 管理變得很是麻煩 進程池能夠幫咱們管理進程 1.進程的建立 2.進程的銷燬 3.任務的分配 4.限制最大的進程數 保證系統正常運行 池內裝的東西有兩種: 裝進程:進程池 裝線程:線程池 進程池 在利用Python進行系統管理的時候,特別是同時操做多個文件目錄,或者遠程控制多臺主機, 並行操做能夠節約大量的時間。多進程是實現併發的手段之一,須要注意的問題是: 1 很明顯須要併發執行的任務一般要遠大於核數 2 一個操做系統不可能無限開啓進程,一般有幾個核就開幾個進程 3 進程開啓過多,效率反而會降低(開啓進程是須要佔用系統資源的,並且開啓多餘核數目的進程也沒法作到並行) 例如當被操做對象數目不大時,能夠直接利用multiprocessing中的Process動態成生多個進程, 十幾個還好,但若是是上百個,上千個。。。手動的去限制進程數量卻又太過繁瑣,此時能夠發揮進程池的功效。 咱們就能夠經過維護一個進程池來控制進程數目,好比httpd的進程模式,規定最小進程數和最大進程數... 建立進程池的類:若是指定numprocess爲3,則進程池會從無到有建立三個進程, 而後自始至終使用這三個進程去執行全部任務,不會開啓其餘進程 使用方式? ThreadPoolExecutor 線程池 實例化 時指定最大線程數 ProcessPoolExecutor 進程池 實例化 時指定最大進程數 執行submit來提交任務 from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor p=ThreadPoolExecutor(4) # 默認開啓的線程數是cpu的核數*5 p.submit(task,i) 總結一下: 進程池能夠自動建立進程 進程限制最大進程數 自動選擇一個空閒的進程幫你處理任務 進程何時算是空閒? 代碼執行完算是空閒 進程池,池子內何時裝進程:併發的任務屬於計算密集型 線程池,池子內何時裝線程:併發的任務屬於IO密集型回調函數: 須要回調函數的場景:進程池中任何一個任務一旦處理完了,就當即告知主進程: 我好了額,你能夠處理個人結果了。主進程則調用一個函數去處理該結果,該函數即回調函數 咱們能夠把耗時間(阻塞)的任務放到進程池中,而後指定回調函數(主進程負責執行), 這樣主進程在執行回調函數時就省去了I/O的過程,直接拿到的是任務的結果。 若是在主進程中等待進程池中全部任務都執行完畢後,再統一處理結果,則無需回調函數