在併發訪問過程當中保持數據一致性是一個很廣泛的問題,而使用鎖則是該問題一個很廣泛的解決方法。全局解釋器鎖(global interpreter lock)如其名運行在解釋器主循環中,在多線程環境下,任何一條線程想要執行代碼的時候,都必須獲取(acquire)到這個鎖,運行必定數量字節碼,而後釋放(release)掉,而後再嘗試獲取。這樣 GIL 就保證了同時只有一條線程在執行。python
通常來講,GIL 並不會帶來麻煩,由於大多數程序的性能瓶頸都在 IO 上(IO-bound)。但當你運行計算密集型代碼,並且計算量又很是大的時候,好比數據分析,你就會發現問題了——你的程序對 CPU 的使用率很是低。以我如今使用的 i5-460m 雙核四線程(2C/4T)處理器來講,Python 最多隻能佔用 25% 的 CPU 負載,正好是一條線程的量。不管是多核心,仍是 intel 的超線程技術,如今提高 CPU 性能的有效手段已經從提升單核性能轉變爲增長核心數量,即執行並行(parallelism)運算。顯然 GIL 的存在與這種思路有些格格不入。編程
若是參考其餘實現的話,你可能會問一個問題,爲何要使用全局鎖,而不是一個更細粒度的鎖呢?實際上 Linux 的文件系統就是這樣作的,進程給目標文件加鎖的時候,能夠只加必定字節數的鎖,只要另外一個進程準備加的鎖與其沒有交集的話,這兩個鎖就能夠共存,這兩個進程也能夠同時修改這一個文件(的不一樣部分)。所以對於 Python,也許能夠給對象加鎖(一切皆對象嘛),同時不限制線程的並行執行。但從網上的信息來看,彷佛這種思路曾經被嘗試實現過,但細粒度的鎖會給單線程模式下的性能形成明顯影響。因此仍是用 GIL 吧~網絡
一般,咱們不會責備某我的笨,但要是他笨還不努力的話可能就得說道說道了。Python 如今的狀況就是,使用這門語言的人基本都已經接受它不如 C 快這一點了,可是對於那些用着 4 核 8 線程,甚至 6 核 12 線程的 CPU 的人來講,看着本身的程序在 10% CPU 負載下執行兩個小時確實是一件很彆扭的事。數據結構
intel 的超線程,是使一個物理核心能夠在極短期內「同時」執行兩條或多條線程的技術,它在操做系統上的體現爲:一個物理核心會被當作兩個邏輯核心使用。這即是當你打開任務管理器,並切換到【性能】標籤頁後,會看到比你 CPU 核心數量多一倍的小方格的緣由。多線程
更加具體的物理實現這裏就不說了,由於我也不懂。下面只經過一些實驗數據來看一下超線程 CPU 在不一樣線程下的效率表現。禁用 HT 的操做必須在 BIOS 中進行,很惋惜筆記本的 BIOS 是簡化過的,沒有這個選項,因此我用本身電腦進行的測試都是運行在 HT enable 的狀態,禁用後的數據來自網絡。併發
以雙核四線程的 i5-460m 爲例,在 Windows 上表現爲 4 個邏輯核心,分別名爲 「CPU 0」,「CPU 1」,「CPU 2」 和 「CPU 3」。其中 0 和 1 是同一個物理核心,2 和 3 是另外一個物理核心。在任務管理器的進程標籤頁中能夠爲指定進程設置處理器相關性,即爲其分配指定的邏輯內核。或者也能夠經過 cmd 命令:start /affinity <mask> .exe
來在程序打開前指定處理器相關性,其中 <mask>
應被替換爲 CPU 編號的 16 進制掩碼。如,爲名爲 test.py
的腳本分配 CPU 0 和 CPU 2 並執行的命令爲:start /affinity 5 python test.py
(2^0 ^+ 2^2 ^= 5)。對於如象棋或 CINEBENCH 這樣的測試軟件,直接在任務管理器中設置便可,出於執行時間考慮,本文使用國際象棋來進行測試。 <br /> ###測試數據(HT 啓用) 單核單線程: <br />函數
<table style="font-size:14px"> <tr> <th>CPU 0</th> <th>CPU 1</th> <th>CPU 2</th> <th>CPU 3</th> </tr> <tr> <td>1645</td> <td>1627</td> <td>1726</td> <td>1704</td> </tr> </table> <br /> **雙核單線程:** <br /> <table style="font-size:14px"> <tr> <th>CPU 0+1</th> <th>CPU 1+2</th> <th>CPU 2+3</th> </tr> <tr> <td>1654</td> <td>1743</td> <td>1735</td> </tr> </table> <br /> 雙核心跑單線程時,CPU 負載會分佈在兩個核心中。這時若是把資源監視器裏面的 CPU 視圖截圖下來,並把兩個核心中的一個(上圖)作垂直旋轉,而後以 50% 透明度疊加到另外一個核心的視圖(下圖)上,就能看到他們基本是徹底互補的。0+1 和 1+2 有少許重疊的陰影存在,緣由不是很肯定,多是其餘進程的負載吧。這樣兩個核心的負載總和只有單邏輯核心的 100%,即 i5-460m 的 25% 。性能
四核單線程:測試
1747優化
雙核雙線程: <br />
<table style="font-size:14px"> <tr> <th>CPU 0+1</th> <th>CPU 0+2</th> <th>CPU 0+3</th> <th>CPU 1+2</th> <th>CPU 1+3</th> <th>CPU 2+3</th> </tr> <tr> <td>2282</td> <td>3537</td> <td>3496</td> <td>3494</td> <td>3526</td> <td>2375</td> </tr> </table> <br /> **四核四線程:**
4472 <br /> ###測試數據(HT 禁用) HT 禁用模式下的數據來源於網絡,由於與個人環境不一樣,這裏直接給結論吧:
<br /> ###結論 上面數據中的看點,或者說比較出乎我意料的地方在於:
想要不改動任何代碼地實現性能提高看來是不可行了。其實對於 GIL 的問題,包括官方給出的最多見的解決方案是:使用多進程,multiprocessing 模塊。
上一節說到本身寫的多進程程序未必比單線程的第三方包快,一方面的緣由在於,計算密集型的擴展包不少都是用 C/C++ 這樣的編譯型語言寫的。而這類擴展包有一個很重要的特性在於,它能夠在被調用時選擇釋放 GIL,從而在不影響主程序的狀況下獨立運算。
但問題在於 C 代碼很差寫,更別說須要使用第三方數據結構的狀況了。這時候在 C 擴展和 Python 之間的一種折中方案就是 Cython,對於 Cython 能夠簡單地將其當作是擁有靜態類型並能嵌入 C 函數的 Python。Cython 也一樣支持釋放 GIL 後的並行。
不管是多進程,仍是編寫擴展,在實行前都得計算一下成本收益,依據能夠節省的時間的量來決定付出多少的努力。對於多數偶發性的狀況,可能優化一下本身的代碼就能帶來很好的效率提高,又沒必要爲此花掉太多時間。所以對於 multiprocessing 和 Cython 的實現細節,這裏就先不深究了。(好水的文)