1. 什麼是GIL?
GIL即全局解釋器(global interpreter lock)。python的每個線程在執行時都需要先獲取GIL,保證同一時刻只有一個線程可以執行代碼,即同一時刻只有持有GIL鎖的線程可以得到執行的機會,使用CPU。
這樣,在跑python多線程程序時,只有當一個線程獲取到全局解釋器鎖GIL後才能運行,而GIL只有一個,因此即使python應用在多核的情況下也只能發揮出單核的性能。
2. 什麼時候會釋放GIL鎖?
1)在做IO操作時,GIL總是被釋放。對所有底層是C代碼的系統來說,GIL會在IO調用之前被釋放,以允許其它的線程在等待IO的時候運行。
2)如果是純計算的程序,沒有IO操作,解釋器會有一個專門ticks進行計數,每隔100次或每隔一定時間(15ms)去釋放GIL,這時線程之間可以開始競爭Gil鎖。
3. 如何解決GIL鎖?
從上面得知,GIL的基本行爲有下面三個:
- 當前執行的線程持有GIL,其他線程得不到執行機會。
- 線程遇到I/O阻塞時,會釋放GIL。(阻塞等待時,就釋放GIL,給另一個線程執行的機會)
- 遇到CPU密集型的線程,解釋器會週期性的進行ticks計數並釋放GIL。
python多線程應用受GIL的影響,不能完全利用多核CPU資源,目前常用的解決方案是:
1) 更換實現語言,cpython爲jpython(不建議),許多模塊不兼容。
2) 使用多進程完成多線程的任務,python多進程沒有GIL問題,每個進程都會擁有一個GIL。
3) 將python中進行密集計算的函數使用C++ 去實現,並在C++ 代碼中主動釋放當前線程的GIL鎖,讓其他線程執行,這也是目前最常用的方案。像許多第三方的擴展模塊(OpenCV),都被設計成在進行密集計算任務時主動釋放GIL。
下面詳細介紹Python GIL導致的問題及解決辦法。
引言
其實一開始並沒有想到研究GIL,但是在研究如何讓你的Python更快的過程中發現我們可以通過這種方式解決掉GIL,讓我們的代碼不被
Python
拖累
這篇博客相比於上面的博客更注重於代碼的講解,我們通過使用pybind11從一個Python
調用C++
的demo出發介紹如何讓Python
調用C++
並且丟棄GIL
GIL簡介
首先我們要知道什麼是GIL
,爲什麼它會拖累Python
,首先我們看一下Python
歷史,Python
是Guido van Rossum 在1989年發佈的,那個時候計算機的主頻還沒有達到1G,程序全部都是運行在單核計算機上面,直到2005年多核處理器才被Intel開發出來
多核處理器意味着什麼呢,就好比一個工廠,你原來只有一個工人幹活,現在有很多個了,一開始設計出來只是爲了能在每個核心上跑不同的應用,但是隨着大家對多核計算機的使用,大家發現有的時候計算器其實很空閒,大部分CPU都在休息,假如只在一個核上跑一個應用的話,那麼其他CPU就浪費了,所以大家就開始設計怎麼並行在多個CPU上跑同樣的任務
現在我們來考慮一下怎麼能讓CPU力往一處使,我們用數據庫來做比方,假設我們計算機上安裝一個銀行數據庫,爲了讓這個“銀行”能夠服務更多的人,我們把對錢的操作(增刪查改)放到每個CPU上運行。假如我們的顧客一個一個排着隊來取錢存錢,我們每個CPU查詢都是唯一的,存取也是唯一的,那麼我們的“銀行”就能正常工作
但是現實的環境往往不是這樣的,顧客它可能會因爲網絡原因個人原因同時進行多個操作,假如它同時取1千萬的兩次操作(它賬號只有1千萬),每個CPU上的程序查詢時候正好都是賬號有一千萬,然後依次進行數據的更新,最後我們發現用戶的賬號變成了0,但是用戶卻取了兩千萬出來,你的銀行損失了一千萬,所以並行任務最重要的就是數據共享
怎麼解決這個共享問題呢,很簡單加“鎖”,我們給需要共享的東西上個鎖,每次你想用的時候你就把鎖鎖上,然後對共享的東西進行操作,當有別人想動這個東西的時候,他一看哎呀有人在用,那我等會。這樣就不會造成上面的衝突了,但是這個也造成了一個問題由於我上了一把鎖,每次我們想操作的時候,必須去看一下這個鎖有沒有被人鎖上,假如沒有我就鎖上,有就等待,這一來一去就會造成一個效率問題(感覺這個也是國企的通病,權利依次掌握在領導上,要想完成工作得不斷的進行開“鎖”、關“鎖”,有時候還會造成“死鎖”),所以並行的4個任務運行速度不一定是一個任務的四倍,所以我們經常看到一些庫在運行說明裏面雙核速度會比單核加速一點幾倍,之說以達不到雙倍就是因爲這些“鎖”的存在
“鎖”幫我們能讓單任務拆分成子任務並行化加速,但是在一定程度上拖累了運行速度,我們回到Python
,因爲多核是在2005年纔出現的,但是在並行化上面,一個比多核更早出現的概率就是:線程
和進程
在還沒有多核處理器的時候,操作系統爲了讓程序並行化跑,就創造了進程和線程的概率。用通俗的話來講,進程就是一家大工廠,而線程就是工人,爲了提高生產力,我們可以開很多家工廠,當然我們也可以開一家工廠,招很多工人。但是線程這個東西相比於進程要消耗的少的多,因爲它“原材料”都是從“工廠”裏面拿的,假如說工廠少了幾個工人還可以生產,但是上萬個工人沒有工廠他們也辦法工作。
所以對於Python
來說首先得支持線程和進程的概率,對於進程來說很簡單,就是多開幾家工廠(多開幾個Python
程序)罷了,但是對於線程來說,由於Python
是一門腳本語言,它需要一個解釋器
來執行代碼,我們知道這個解釋器它可以當做大一個共享變量,假如在不同的線程裏面用“鎖”來限制一下的話,環境變量就會亂了套
所以Python
對於線程的支持就是給他加一個鎖,也就是我們俗稱的GIL
,由於在操作系統在運行單核的時候就支持線程,一個工人加一個鎖其實也沒有什麼,無非就是多了一點開鎖關鎖的時間,所以Python
在2005前一直沒有GIL
這個概率,到了2005大家發現Python
使用多線程竟然只能使用一個核,完全浪費了其他核,因爲雖然Python
的線程可以分配到不同的核上運行,但是當他們運行的時候發現這個鎖沒有被釋放,所以每個核上的線程都傻乎乎的在等待,結果最後查看效果多線程比單線程速度還慢(要等GIL
釋放)
Python
社區逐漸發現這個問題,他們也做了很多挽救工作,比如在線程睡覺(sleep)、等待連接的時候讓線程主動釋放GIL
,這樣就能讓其他線程繼續執行,但是對於純粹的運算代碼而不是IO密集代碼總也避不開這個鎖的存在,如果允許GIL
釋放,由於歷史遺留問題很多代碼都會亂了套(理論上其實就是需要重新修改鎖的設計,可以參考MySQL的代碼去掉“鎖”花了5年時間),考慮到Python
本來就運行的慢,Python
開發者覺得假如你覺得代碼很慢,你可以放到C/C++
裏面執行,所以對於這個GIL
就沒有繼續啃下去,而是把中心放在Python
調用C/C++
中,提供了一些很方便的方式讓我們在C/C++
中控制GIL
的釋放以及獲取
所以我們接下來通過一個來學習Python
調用C++
代碼,來了解Python
如何調用C++
,並且通過一些實驗來驗證線程、進程和GIL。
測試GIL的存在
首先我們要做的第一件事就是測試GIL的存在,現在基本上主流電腦都是多核CPU,所以我們這個實驗可以很輕鬆的在多核下進行
首先我們得安裝一些環境:Python3
,gcc
,htop
(在Windows可以用下任務管理器代替)
首先我得提一下我的一個認識誤區,在以前我不太清楚線程、進程與多核直接的關係的時候我有一個誤區,我以爲C
能在單線程裏面使用多核(我也不清楚爲什麼我會這麼想,可能是因爲了解很少),而Python
卻不能,後面通過我實驗我才發現,無論是C
和Python
只要你的代碼不使用線程、進程那麼你的代碼只能同時運行在同一個核上
怎麼來測試呢,我們可以在Python
的解釋器裏面輸入
while True:
pass
然後我們打開htop
,我們可以發現某一個CPU
始終保持在100%(這個CPU可能會變化,因爲操作系統控制每個進程切換CPU時間),假如你沒有其他任務過多使用CPU
的話,你其他的核心一直保持在很低的利用率,當你ctrl-c
你的代碼後,那個100%的CUP會立馬降下來
然後你在編譯一個C
程序,使用gcc a.c && ./a.out
命令編譯下面代碼然後運行
// a.c
int main(){while(1){};}
你會發現C
也只能消耗一個CPU,這就印證了我們前面說過得,如果我們不主動使用線程或進程來,同時只能有一個在運行
接下來我們看看在多進程的基礎上,使用Python
來使用多核
from concurrent.futures import ProcessPoolExecutor
def f(a):
while 1:
pass
if __name__ == '__main__':
pool = ProcessPoolExecutor()
pool.map(f, range(100))
當我們運行上面代碼的時候,我們會發現所有CPU
會運行到100%,我們只要簡單聲明一個進程池(ProcessPoolExecutor
),Python
自動幫我們生成你CPU核數相同的進程,然後我們只要把任務分配到池中就能重複的並行化任務,把所有的核心都用起來。
然後我們來測試一下線程池,要使用Python
線程池只需要初始化ThreadPoolExecutor
就行
from concurrent.futures import ThreadPoolExecutor
def f(a):
while 1:
pass
if __name__ == '__main__':
pool = ThreadPoolExecutor()
pool.map(f, range(100))
我們從htop
可以看到在Python
線程中,只有一個能達到100%,這就是GIL
的“威力”,它讓我們多線程沒有發揮多線程的力量,重複使用到多核CPU
接下來我們看看在C++
裏面使用多線程是否能夠發揮多核的威力
// run.cpp
#include <thread>
using namespace std;
#define NUM_THREADS 50
void f(){
while(1){};
}
void run_dead(){
std::thread threads[NUM_THREADS];
for(int i = 0; i < NUM_THREADS; ++i)
{
threads[i] = std::thread(f);
}
for (int i = 0; i < NUM_THREADS; ++i) {
threads[i].join();
}
};
int main(void){
run_dead();
}
我們使用g++ -pthread -std=c++11 run.cpp && ./a.out
運行上面的C++
程序,我們在htop裏面能夠發現,C++
的多線程能夠完全發揮多核的威力
上面的程序都很簡單,但是具備一個多線程運行的基本構造,我們可以修改我們的調用的子任務來完成實際的任務,當然你程序越複雜也涉及到了各種鎖的使用,這裏我們就不談了
從上面的程序我們可以知道C++
的多線程能夠充分使用多核,而Python
的不行,接下來我們就開始探索Python
調用C++
Python調用C++
在上面的博客我總結了Python
調用C++
的方式,總的來說Cython
是控制能力最好的,效率也是最高的,但是由於存在一個學習新語言的難度,所以我這裏就不提了,改天再寫一篇關於Cython
的博客,我們這裏使用pybind11這個庫作爲介紹
安裝非常簡單pip install pybind11
就行,接下來我們使用github上這個官方例子做介紹,最後我們以一個實際的C++
項目爲例子,看看如何在實際的項目使用
首先我們先把項目下載下來
git clone https://github.com/pybind/python_example.git
然後我們新建一個環境(避免安裝到我們系統的環境,方便刪除)
python -m venv venv
PS: 當前Python
版本默認爲py3.5以上(你可以使用pyenv安裝Python多個版本,目前我在自己使用Python版本,但主要使用3.6以上)
source venv/bin/activate
然後我們激活我們的環境,我們順便安裝一下我們接下來要安裝的Python
包
pip install ipython
然後我們進入項目cd python_example
,假如你用Pycharm
的話,你可以在項目目錄下生成venv
環境,然後在Pycharm
裏面打開會自動設定爲默認環境
然後我們先測試一下代碼可以不可以用
pip install .
假如我們安裝成功了,恭喜你,我們的環境已經準備好了,打開ipython
,我們先測試一下這個C++
代碼的速度
In [1]: import python_example
In [2]: python_example.add(1, 1)
Out[2]: 2
很好,代碼運行正常,就是一個簡單的加法運算,我們測試一下平均時間
In [3]: %timeit python_example.add(1, 1)
313 ns ± 3.03 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
很好,我們的C
代碼還是跑到很快,313納秒就跑完了,接下來我們看看純粹的Python
代碼速度
In [4]: def add(a, b):
...: return a + b
...:
...:
In [5]: %timeit add(1, 1)
113 ns ± 9.04 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
什麼竟然比C++
還要快,快了近3倍,記得我當時第一次運行出來的這個結果的時候的震驚,說好的快呢,你騙我。
接下來我們就來分析一下出現這個的原因,會不會是因爲類型轉換出現問題呢,因爲pyblind11使用了很多自動轉換的技術來幫我們轉換,我們看看原函數(在src/main.cpp)
int add(int i, int j) {
return i + j;
}
首先Python
調用它,要把第一個參數由Python
的int
對象轉換成C++
的int
基本類型,C++
運行完之後,又得轉換將C++
基本int
類型轉換成Python
的int
對象,這一來一回就得多花三個操作,爲了驗證我們猜想,我們插入一個nothing
函數在add
函數後面
void nothing(){
}
然後模仿m.def
仿照寫一行插入nothing
函數(你會發現語法特別簡單,這也是我喜歡pyblind11的原因)
m.def("nothing", ¬hing, R"pbdoc(
do nothing
)pbdoc");
接下來我們安裝一下我們的新庫pip install .
然後我們再開一個新的ipython
(你可以用importlib
來重新加載庫)
In [1]: import python_example
In [2]: %timeit python_example.nothing()
125 ns ± 0.6 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
125ns,我們的猜想成功了,類型轉換的確拖累了C++
運行的速度,我們再看看原生的速度如何
In [4]: def nothing():
...: pass
...:
...:
In [5]: %timeit nothing()
85.1 ns ± 0.262 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
竟然還是比C++
快,雖然沒有上面那麼誇張,但是快了25%,我們再來分析原因,首先現在沒有類型轉換所以理論上那隻能是代碼運行問題,我們知道Python
優化裏面提過一句,少用.
,因爲Python
要搜尋很多東西才能獲得到對象的屬性、方法等,所以我們這邊使用了python_example.nothing
來調用nothing
函數,假如我們去掉.
速度會不會提高呢
怎麼去掉呢,用局部變量
In [6]: pn = python_example.nothing
In [7]: %timeit pn()
90 ns ± 0.761 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
從上面可以看到的確,.
“害人不淺”,我們的速度又快了一大截,基本上同原生沒有太多差距了,一開始我以爲是概率問題,運行了多次但是結果都是一樣,原生就是比C++
快了5ns,可能是pyblind11“偷偷”的在哪個地方偷跑了一條語句吧,或者有可能是C++
比C
(Python
是C
寫的)稍微慢了一點
一開始我以爲C++
一定會比Python
快,但是我們從上面測試可以看出來,在“起跑”階段,C++
甚至比Python
要慢,我們使用C++
主要是爲了加速大段Python
代碼,只要在這場“長征”中C++
能夠勝出,那麼我們的努力就沒白費,那好我們繼續測試,看看在長征過程中C++
表現如何
首先我們把add
函數魔改一下,我們讓他進行100次運算
int add(int i, int j) {
int s = 0, x = 0;
for(;x<100;x++){
s = s + i + j;
}
return s;
}
我們再把模塊給安裝一下pip install .
,重新打開新的ipython
In [1]: import python_example
In [2]: python_example.add(1,2)
Out[2]: 300
In [3]: padd = python_example.add
In [4]: %timeit padd(1,1)
282 ns ± 3.9 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
In [5]: %timeit python_example.add(1,1)
316 ns ± 3.78 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
我們這次重要見到了C++
的威力,我們進行100次運算,相比於上面一次運算,我們只增加了4ns
的平均時間,我們來看看原生Python
的表現如何
In [6]: def add(a, b):
...: s = 0
...: for i in range(100):
...: s += a + b
...: return s
...:
...:
In [7]: add(1, 2)
Out[7]: 300
In [8]: %timeit add(1, 1)
4.74 µs ± 40.5 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
C++
完爆Python
,4.74us = 4750ns
,Python
用時是C++
的10倍多,只還只是100次運算,假如我們上萬次運算,那結果更加誇張,C++
在長征的過程中勝利了,但是我們也不能說Python
是慢畢竟us
的單位其實非常小,1us=1000ms=1000000s
,在1s內可以執行上面函數幾十萬次,只能說C++
速度太可怕了
調用總結
我們從上面可以看到,雖然Python
調用C++
在類型轉換上會有速度損失,但是在進入到函數提內運行過程中的速度是不影響的,假如我們的運算量夠大,完全可以彌補那一點點性能影響,所以要想重複利用C++
的速度,儘量少調用C++
,把計算結果竟然一次性返回,而不是我們多次進行交互,這樣就能最大化利用C++
在C++
線程中測試GIL
接下來我們來考慮這麼一個問題,前面我們測試了C++
的線程能使用多核,我們假如在讓Python
在調用C++
的代碼中中使用線程,那麼我們的C++
的線程能不能使用多核呢進而解除GIL的作用
我們把nothing
函數改成多線程
#include <thread>
#define NUM_THREADS 50
using namespace std;
void f(){
while(1){};
}
void nothing(){
std::thread threads[NUM_THREADS];
for(int i = 0; i < NUM_THREADS; ++i)
{
threads[i] = std::thread(f);
}
for (int i = 0; i < NUM_THREADS; ++i) {
threads[i].join();
}
}
然後我們再重新編譯一下pip install .
,我們來跑一下我們這個多線程的nothing
函數
In [1]: import python_example
In [2]: python_example.nothing()
我們在htop
裏面可以看到在單線程的Python
程序中,成功的將所有核心都利用上了,也就是是說假如我們在C++
擴展中使用線程的話,是不會被GIL
影響的
說實話當我第一次運行的時候我直覺是還是會被GIL
影響,結果最後跑出來的結果大吃我一驚,現在我們分析爲什麼不會被受影響,因爲GIL
鎖的是Python
解釋器,當我們的代碼進入到C++
中的時候,我們已經不在Python
解釋器中了,這樣即使我在C++
中聲明線程,那也是C++
的線程,所以就不會造成無法使用多核的情況
這裏我們學到一點,如果我們想擺脫GIL
可以把線程放到C++
中,這樣線程的不再依賴Python
解釋器,前面我們知道其實Python
底層是用C
寫的,所以基本上所以的語法都是基於C
代碼實現加上語法糖來完成的,Python
線程也就是C
線程,我們能不能模擬一下Python
來構建這個GIL
首先我們知道GIL
是一把鎖,所以我們第一件事就是查看這把鎖,在這裏我們通過Python
的C
頭文件來引入一個函數PyGILState_Check
這個函數會返回一個1
和0
值,假如是1
那麼意思該線程拿着GIL
鎖,反之。
所以我們先在頭部加上#include "Python.h"
,在Linux系統上要安裝python-dev
或者python-devel
開發包纔有這個頭文件,接下來我們在nothing
函數加上這個檢測狀態
cout << "GIL is " << ((PyGILState_Check() == 1) ? "hold" : "not hold")<<endl;
提一句爲了使用cout
,我們得在頭部加上C++
輸出庫#include <iostream>
先在我們重新安裝一下並運行nothing
函數,程序會輸出GIL is hold
,爲什麼會出現這個情況呢,因爲Python
默認會鎖住GIL
當運行C++
或者C
代碼的時候,但是爲什麼我們雖然鎖住了GIL
但是我們還是能夠使用C++
的線程來運行多核呢,其實很簡單因爲我們的線程沒有像Python
一樣每次運行的時候去獲取這個GIL
鎖,爲了證明這一點,我們來做個實驗
首先我們得在nothing
函數裏面釋放GIL
,然後讓線程去獲取GIL
(如果nothing
主函數不釋放GIL
,會造成死鎖,線程無法運行,一直獲取不了GIL
鎖),我們可以用Python
的C
頭文件的函數來釋放GIL
鎖,但是pybind11提供了一個更加方便的函數讓我們來釋放GIL
鎖,我們把nothing
函數定義修改一下,在後面添加一條語句py::call_guard<py::gil_scoped_release>()
// m.def("nothing", ¬hing);
m.def("nothing", ¬hing, py::call_guard<py::gil_scoped_release>());
然後我們在重新編譯安裝運行一下代碼,我們的結果就會是GIL is not hold
,我們通過簡單的一條語句就釋放GIL
鎖,接下來我們來測試在線程中獲取GIL
鎖來模擬Python
的情況
要想獲取GIL
鎖,pybind11也提供了一個非常簡單的方法來實現這個:py::gil_scoped_acquire acquire;
我們接下來把f
函數改成下面的
void f(){
cout << "entner F: GIL is " << ((PyGILState_Check() == 1) ? "hold" : "not hold")<<endl;
py::gil_scoped_acquire acquire;
cout << "GIL is " << ((PyGILState_Check() == 1) ? "hold" : "not hold") << " now is runing "<<endl;
while(1) {
};
}
我們在獲取GIL
前後,添加了一些輸出,方便我們調試,接下來我們再運行我們的代碼,我們發現程序輸出50個進入entner F: GIL is not hold
(在我的電腦上,因爲線程同時運行,獲取GIL
鎖需要時間,所以在我電腦上每次運行f
函數時鎖都打開着),但是隻有一行GIL is hold now is runing
,因爲當一個線程獲取到GIL
後,其他線程就沒法獲取到了,而且看htop
我們也能發現只有一個核到了100
,在我們強行模擬下C++
也沒能使用多核
其實從這裏我們可以看出來,GIL
問題其實就是一個死鎖的問題,線程獲取後不釋放鎖,導致所有線程相互競爭,用一個諺語來說就是:一個和尚挑水喝、兩個和尚擡水喝、三個和尚沒水喝。
那麼我們怎麼來解決這個問題呢,很簡單就是在你不需要的鎖的時候去釋放它,接下來我們來模擬一下怎麼釋放這個鎖達到多線程“和平共處”,首先我們引入C++
時間庫來使用sleep
函數(#include <unistd.h>
),接下來我們引入Python
的C
頭文件中的宏來釋放GIL
,我們把f
函數改成下面的形式
void f(){
cout << "entner F: GIL is " << ((PyGILState_Check() == 1) ? "hold" : "not hold")<<endl;
py::gil_scoped_acquire acquire;
cout << "GIL is " << ((PyGILState_Check() == 1) ? "hold" : "not hold") << " now is runing "<<endl;
Py_BEGIN_ALLOW_THREADS
while(1){
};
Py_END_ALLOW_THREADS
}
我們使用Py_BEGIN_ALLOW_THREADS
和Py_END_ALLOW_THREADS
這一對宏來釋放GIL
,這樣我們重新編譯運行nothing
函數我們就能看到50個enter
和50個runing
,而且在htop
中我們也能發現C++
的線程再次使用所有的核心了(利用率達到不了100%,不知道是因爲宏的“副作用”還是其他原因,但是每個核還是能夠到70%作用),這種在一個函數中獲取和釋放GIL
鎖還是不推薦的,最好在函數一開始的時候釋放GIL
,在函數結束的時候獲取GIL
返回到Python
解釋器中(假如你需要與Python
進行交互的話),畢竟獲取一次鎖的成本還是挺大的,而且一不小心就會造成死鎖
在Python
線程中測試GIL
接下來我們來看看一個已經存在的問題,就是如何解決掉使用Python
線程時遇到的GIL
問題,其實我們在上面的C++
線程已經模擬出來了,解決這個問題的關鍵就是釋放GIL
鎖,我們先測試一下在GIL
鎖下,線程調用C++
代碼的速度
我們首先添加一個新死循環函數
void run_dead(){
while(1){};
}
然後在後面加上pybind11的定義
m.def("run_dead", &run_dead);
接着我們運行下面函數
from concurrent.futures import ThreadPoolExecutor
import python_example
pool = ThreadPoolExecutor()
for i in range(100):
pool.submit(python_example.run_dead)
在這個函數裏面我們聲明瞭一個線程池,並且向池蕾加入了100函數,接着我們在htop
裏面查看CPU利用率,我們可以看到只有1個CPU能夠跑滿100%,其實從前面的實驗我們就能猜到這個結果,解決方案其實前面也給了,有兩種方法,第一種就是使用Python
的C的頭文件函數宏
:Py_BEGIN_ALLOW_THREADS
和Py_END_ALLOW_THREADS
,第二種就是在函數聲明的地方使用 pybind11提供的py::call_guard<py::gil_scoped_release>()
來釋放GIL
,兩種方法都可以,但是第二種更加簡單一點,在這裏我就不測試釋放GIL
之後的性能了,前面已經做過了
GIL
總結
通過前面我們的測試,GIL
這個東西其實只是一把鎖,我們經常能聽到很多人抨擊Python
關於GIL
問題,這就給人一種錯覺Python
這種語言在設計上有弊端,在前面測試我們也發現了就算是C++
或者C
假如不正確的使用鎖其實也會有這個GIL
問題,GIL
的問題的並不是“編程語言”的鍋,主要是我們自己的代碼造成的死鎖,所以面對GIL
的時候,不需要困惑,它就是一把“鎖”,把它打開,而不是碰到它就跑,你會發現它也就是一把“鎖”而已。