python hack_Python进阶:深入GIL(上篇)
Python進階:深入GIL(上篇)HackPython致力于有趣有價值的編程教學
簡介
熟悉Python的人理應都聽過GIL(Global Interpreter Lock,全局解釋器鎖) ,大概也知道它就是造成Python多線程并發其實是「偽并行」的核心原因,但依舊很多人沒有深入其中,所以HackPython嘗試以上、下兩篇文章來闡釋GIL,分別從其表現現象、對應源碼以及Python對GIL改進等方面進行討論 。
線程與進程
在討論GIL前,先通過簡單的文字描述一下線程與進程并理解其中的關系。
當我們啟動一個程序時,系統中就至少啟動了一個對應的進程,即一個程序至少對應一個進程 。所謂程序,它其實是一種靜態的資源實體,就是一堆代碼,存在于硬盤中,本身沒有任何運行的含義,而進程是動態的,它是程序操作某個數據集時的動態實體,存在于內存中 。
一個進程可以包含多個線程,這些線程可以共享當前進程中的內存空間 ,這種特性就出現了線程不安全的概念,即多個線程同時使用了一個空間,導致程序邏輯錯誤 ,常見的方式就是使用鎖或信號量等機制來限制公共資源的使用 。
Python多線程的偽并行
Python中可以使用「threading」模塊來創建并使用多線程,為了直觀比較,先試一下一個沒有使用多線程的代碼 ,如下:import time
def add(n):
sum = 0
while sum <= n:
sum += 1
print(f'sum:{sum}')
if __name__ == '__main__':
start = time.time()
add(500000000)
print('run time: %s'%str(time.time() - start))
代碼非常簡單,就是一個add()方法一直做累加操作,運行結果為 :python 5.py
sum:500000001
run time: 23.80576515197754
那我使用多線程效果會不會好一些呢?憑感覺直觀而言,應該是會的 ,因為上面的程序只使用了一個線程,那我開兩個線程,讓其同時工作,其運行時間應該短一半才對 ,但事實時使用多線程后,運行時間依舊沒有變動 ,多線程版本的代碼如下:import threading, time
def add(n):
sum = 0
while sum <= n:
sum += 1
print(f'sum:{sum}')
if __name__ == '__main__':
start = time.time()
n = 500000000
t1 = threading.Thread(target=add, args=[n//2])
t2 = threading.Thread(target=add, args=[n//2])
t1.start()
t2.start()
t1.join()
t2.join()
print('run time: %s'%str(time.time() - start))
為了讓相加的數量相近,這里每個線程只需要執行「n//2」次,使用join的目的是得等線程運行完后,再執行后續的邏輯,這里只是為了方便記錄運行時間 。運行結果如下:python 6.py
sum:250000001
sum:250000001
run time: 23.04693603515625
發現跟一開始單線程的程序在運行時間上沒有什么差異,而造成這種現象的原因就是GIL ,需要注意的是,GIL只存在于通過C語言實現的Python解釋器上,即CPython上 ,后人為了繞過GIL的問題利用Java開發了Jpython或使用Python自己開發了自己的解釋器PyPy,這些上都不存在GIL全局解釋器鎖的問題 ,但CPython才是當前最多人使用的主流Python解釋器 。
在CPython中,每一個Python線程執行前都需要去獲得GIL鎖 ,獲得該鎖的線程才可以執行,沒有獲得的只能等待 ,當具有GIL鎖的線程運行完成后,其他等待的線程就會去爭奪GIL鎖,這就造成了,在Python中使用多線程,但同一時刻下依舊只有一個線程在運行 ,所以Python多線程其實并不是「并行」的,而是「并發」 。
看到下圖,圖中是Python中GIL的工作實例,其中有3個線程,線程與線程之間是順序執行的 ,每個線程開始執行時都會去獲得GIL,防止其他線程線程運行 ,每執行完一段時間后,就會釋放GIL,讓別的線程可以去爭奪執行權限,如果自己本身也沒有執行完,則本身也會參與這次爭奪 。
可以發現,Python中的線程工作一段時間后,會主動釋放GIL,這是為了讓其他線程都有機會執行 ,而釋放的時機就涉及到了「檢查間隔」(check interval)機制 ,在早期版本的Python中,檢查機制是100ticks,而Python3后,每15毫米使用一次檢查間隔,然后就會釋放GIL鎖 。
但需要注意的是線程有了GIL后并不意味著使用Python多線程時不需要考慮線程安全 ,「GIL的存在是為了方便使用C語言編寫CPython解釋器的編寫者,而頂層使用Python時依舊要考慮線程安全」 ,在下一篇中會從原始編碼層面來解釋存在GIL后,依舊會有線程不安全現象的原因。
多進程實現并行
GIL的存在讓Python多線程在運行CPU密集型性程序時顯得非常無力,為了繞過GIL的限制,一種簡單的方法就是使用多進程 ,這是因為GIL只會存在于線程級別,即一個進程為了確保某一時刻下只有一個線程在運行,才使用GIL ,但多個進程之間并不會出現這種限制,不同的進程會運行在CPU不同的核上,實現真正的「并行」 。
通過進程的方式將上面的任務再執行一遍,看一下運行時長,具體代碼如下:from multiprocessing import Process
import time
def add(procname, n):
sum = 0
while sum <= n:
sum += 1
print(f'process name: {procname}')
print(f'sum: {sum}')
if __name__ == '__main__':
start = time.time()
n = 500000000
p1 = Process(target=add, args=('Proc-1',n//2))
p2 = Process(target=add, args=('Proc-2',n//2))
p1.start()
p2.start()
p1.join()
p2.join()
print('run time: %s'%str(time.time() - start))
Python中多進程可以使用 multiprocessing 這個庫,使用方法與使用線程類似 ,代碼中啟用了兩個進程,分別運行 n//2 數據量的數據,其結果如下:python 7.py
process name: Proc-1
sum: 250000001
process name: Proc-2
sum: 250000001
run time: 12.768253087997437
從結果可以看出,時間確實減少了一半左右,多進程狀態下確實是真正的「并行」。
如何繞過GIL?
有了多進程后,大部分程序都可以通過多進程的方式繞過GIL ,但如果依舊不滿足,就需要使用C/C++來實現這部分代碼,并生成對應的so或dll文件,再通過Python的ctypes將其調用起來 ,Python中很多對計算性能有較高要求的庫都采用了這種方式,如Numpy、Pandas等等 。
如果你對程序的性能要求的特別嚴格,此時更好的方法是選擇其他語言 。
結尾
本節簡單的討論了Python中GIL相關的內容,在下一篇中會從代碼層面再次深入的討論GIL的相關內容,歡迎學習 HackPython 的教學課程并感覺您的閱讀與支持。
參考文章:
總結
以上是生活随笔為你收集整理的python hack_Python进阶:深入GIL(上篇)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: html怎样在一张图片里写字,用HTML
- 下一篇: python全局变量赋值_Python全