1. 程式人生 > >再談Python多執行緒--避免GIL對效能的影響

再談Python多執行緒--避免GIL對效能的影響

在Python中使用多執行緒,如果你對GIL本身沒有一定的瞭解;那麼很有可能你只是寫出了正確的多執行緒程式碼,而並沒有達到多執行緒的目的,甚至截然相反的效果。下面介紹了Python中GIL的作用和侷限性,並提供了避免GIL影響效能的幾個建議。

GIL是CPython中特有的全域性直譯器鎖(其它實現版本因為有自己執行緒排程機制,所以沒有GIL機制)。本質上講它就是Python程序中的一把超大鎖。這把鎖在直譯器程序中是全域性有效的,它主要鎖定Python執行緒的CPU執行資源。

換句話說,在CPython直譯器中當一個執行緒需要執行CPU進行計算之前,它需要先獲得這把大鎖;否則即使已經被作業系統排程出來,但仍然無法執行計算。所以CPython直譯器中,執行緒的想要執行CPU指令需要2個條件:

  1. 被作業系統排程出來【作業系統允許它佔用CPU】
  2. 獲取到GIL【CPython直譯器允許它執行指令】

非常不幸的是,我們並不總是能滿足這2個條件。經常出現的情況是:已經滿足條件1,卻被條件2限制。而這就是GIL影響Python效能的主要原因【其它語言只需滿足條件1即可】。

如果Python(這裡預設指CPython)在單核CPU的機器上執行,它的多執行緒與單執行緒、以及其它語言的多執行緒在本質上並沒有什麼不一樣。【所有執行緒都是輪流佔用CPU執行指令】

而如果Python在多核CPU機器上執行的時候,效能則會非常槽糕。主要原因是在單核的時候,同時只有一個執行緒在執行CPU,所以這個執行緒總是能獲取到GIL。而換到多核的時候,同時會有多個執行緒在不同的CPU核心上執行,此時不同執行緒之間就需要競爭GIL,而GIL只能同時被一個執行緒申請到,所以會導致其它執行緒處於閒置狀態【即使它已經擁有了CPU資源】。所以Python在多核CPU上的多執行緒始終只有單執行緒在跑程式。


在早期的Python版本(3.2之前)中,GIL除了會讓多執行緒在多核機器下表現槽糕外,它還會導致某些執行緒場景佔用GIL,而其它執行緒卻無法申請到。典型場景是:

  • 在2個執行緒的情況下
  • 一個是IO密集型執行緒
  • 一個是計算密集型執行緒

那麼最後的結果便是,一旦計算密集型執行緒獲得了GIL,那麼它在很長一段時間內都將佔據GIL,甚至一直到該執行緒執行結束。因為計算密集型執行緒在釋放GIL之後又會立即去申請GIL,並且通常在其它執行緒還沒有排程完之前它就已經重新獲取到了GIL。【因為GIL它沒有鎖通知機制,比如:Condition鎖】

在Python3.4之後,由於對GIL有了較大的改進。在單核的情況下,對於單個執行緒長期佔用GIL的情況有所好轉;但是在多核的情況下,效能仍然還是沒有多大的改善。

很多時候有些事情我們無法改變,但是生活給了我們智慧;所以為了客服困難,我們始終還是能找到解決辦法的。雖然預設情況下GIL對Python多執行緒的多核的情況下有較大的效能影響,但是為了能在Python中利用多核來提高計算效率,還是有如下的方法可以實現的:

  • 使用python3.4或更高版本(對GIL機制進行了優化)
  • 使用多程序替換多執行緒(多程序之間沒有GIL,但是程序本身的資源消耗較多)
  • 使用C編寫高效能模組(with nogil調出GIL限制)
  • 指定cpu執行執行緒(使用affinity模組)
  • 使用Jython、IronPython等無GIL直譯器
  • 全IO密集型任務時使用多執行緒
  • 使用協程(高效的單執行緒模式,也稱微執行緒;通常與多程序配合使用)

關於GIL的實現原理、改進內容、適用場景等知識點,可以檢視這裡