c语言的锁和Python锁,Python中全局解释器锁、多线程和多进程
全局解釋器鎖(GIL)只允許1個Python線程控制Python解釋器。這也就意味著同一時間點只能有1個Python線程運行。如果你的Python程序只有一個線程,那么全局解釋器鎖可能對你的影響不大,但是如果你的程序是CPU密集型同時使用了多線程,那么程序運行可能會受到很大影響。全局解釋器鎖在Python中飽受詬病,但是它確實為Python內存處理提供了方便。
什么是全局解釋器鎖
Python通過引用計數的方法來管理內存,每一個內存對象都會有一個引用計數,如果一個對象的引用計數變為0,那么該對象所占用的內存就會被釋放。比如
import sys
a = []
b = a
sys.getrefcount(a)
## 3
在上述代碼中,空列表對象在內存中被引用了3次,即分別被a和b引用,同時被sys.getrefcount()調用。
但是,如果一個對象同時被多個線程來引用,那么引用計數可能同時增加或減少,每個線程按照自己的方式進行計數,對象在整個內存中的引用變得十分混亂,很容易造成內存泄漏或者其他很多不可預見的Bug。一個解決辦法給就是每個線程都給引用計數加一個鎖,阻止別人修改,不過這樣會造成鎖死現象(比如一個對象有多個鎖時),另外,大量的資源會浪費在加鎖解鎖的過程了,嚴重拖慢了程序運行速度。
這個時候,就需要一個統一來管理引用計數的機制,以確保對象引用計數準確、安全。全局解釋器鎖就是用來處理這種情況的。它既避免了不同線程帶來的引用計數混亂,又避免了過多線程鎖帶來的死鎖和運行效率低的問題。
雖然全局解釋器鎖解決了對象引用計數的問題,但隨之而來的是,很多CPU密集型任務在全局解釋器鎖的作用下,實際上變成了單線程,不能充分發揮CPU的算力,影響程序速度。
全局解釋器鎖并不是Python獨有的,其他一些語言,比如Ruby也存在全局解釋器鎖。還有一些語言沒有使用引用計數的方式來管理內容,而是使用垃圾回收機制(GC)來管理內存。雖然這樣避免了全局解釋器鎖,但是在單線程處理上,GC并不占有優勢。
為什么Python選擇全局解釋器鎖
Python在設計之初,就選擇了全局解釋器鎖用來管理內存引用計數,在當時,操作系統還沒有線程的概念,所以全局解釋器鎖并沒有帶了弊端,反而給開發者帶來了很多方便。
很多Python的擴展都是使用C語言庫來編寫的,這些C編寫的擴展需要全局解釋器鎖來確保線程的內存安全。即便是有些C語言庫內存處理上不是很安全,那么在Python中全局解釋器鎖的作用下,也能很好的發揮作用。
所以,全局解釋器鎖對早期使用CPython做解釋器的開發者來說,解決了很多內存管理的問題。
全局解釋器鎖對多線程的影響
首先我們應該區分不同性質的任務,有一些是CPU密集型的任務,有一些是I/O密集型的任務。
CPU密集型的任務在最大程度上使用了CPU,比如數學矩陣的計算、圖像處理、文件解壓縮等。
I/O密集型任務在需要花費很多時間來等待信息的輸入和輸出(讀寫),比如從網址下載內容,大量訪問磁盤進行讀寫等。
如果是CPU密集型任務,比如下面我們進行倒計時,使用單線程:
# 單線程
import time
from threading import Thread
COUNT = 50000000
def countdown(n):
while n>0:
n -= 1
start = time.time()
countdown(COUNT)
end = time.time()
print('Time taken in seconds -', end - start)
# Time taken in seconds - 6.20024037361145
最后倒計時完成,一共用了6.2秒
使用多線程
# 使用多線程
import time
from threading import Thread
COUNT = 50000000
def countdown(n):
while n>0:
n -= 1
t1 = Thread(target=countdown, args=(COUNT//2,))
t2 = Thread(target=countdown, args=(COUNT//2,))
start = time.time()
t1.start()
t2.start()
t1.join()
t2.join()
end = time.time()
print('Time taken in seconds -', end - start)
# Time taken in seconds - 6.924342632293701
最后用了6.9秒!
可以看出來,對于這個CPU密集型的任務,多線程使用的時間竟然比單線程還多,使用多線程并沒有達到我們縮短運行時間的目的。這就是全局解釋器鎖帶來的問題。但是如果是I/O密集型的任務,全局解釋器鎖就不會再有這種影響,各個線程在等待I/O的時候可以共享鎖。
為什么全局解釋器鎖還保留在Python中
首先全局解釋器鎖一直在Python中使用,如果在某個版本中移除了全局解釋器鎖,會帶來各種兼容性問題,尤其是對于Python這種有大量開發者的編程語言來說,稍微一點變動都會帶來巨大的影響。另外,很多C語言編寫的擴展嚴重依賴于局解釋器鎖。況且,如果移除了全局解釋器鎖,那么又可能會造成單線程或者I/O多線程處理速度的下降。
當Python從2.0升級到3.0的大版本升級時,實際上是有機遇改變全局解釋器鎖的。但是這使得Python3在處理單線程或者I/O多線程時,比Python2還慢!所以Python3中依舊保留了全局解釋器鎖。當然,在Python3中,對全局解釋器鎖進行了修改和優化,即減少I/O所線程使用全局解釋器鎖的機會,把更多的時間和機會留給CPU密集型多線程任務。另外,如果一個線程持續使用全局解釋器鎖一段時間后,程序會強制其釋放全局解釋器鎖,當然如果沒有其他線程請求使用全局解釋器鎖,那么該線程還可以繼續使用全局解釋器鎖。
我們應該應對全局解釋器鎖
既然全局解釋器鎖帶來一些問題,那么我們怎么來解決這些問題?這兒我們要說兩個不同的概念:
多進程(multi-processing) 和多線程(multi-threading):
多進程是各個并行任務之間“不使用“共同的內存空間;而多線程的各個并行任務”使用“共同的內存空間。
1)多進程
優點:獨立內存空間;實現代碼直觀簡單;充分利用多核多CPU;避免全局解釋器鎖的限制;
缺點:無法實現對象和內容共享;需要較大的內存空間
2)多線程
優點:輕量,需要的額外內存較小;共享內存,方便訪問;對于CPython解釋器,可以通過全局解釋器鎖使用C擴展;適合I/O密集型任務
缺點:全局解釋器鎖的限制;并行任務不能殺掉;實現代碼較為復雜
我們通過多進程的形式來實現上面例子中的倒計時,
from multiprocessing import Pool
import time
COUNT = 50000000
def countdown(n):
while n>0:
n -= 1
if __name__ == '__main__':
pool = Pool(processes=2)
start = time.time()
r1 = pool.apply_async(countdown, [COUNT//2])
r2 = pool.apply_async(countdown, [COUNT//2])
pool.close()
pool.join()
end = time.time()
print('Time taken in seconds -', end - start)
# Time taken in seconds - 4.060242414474487
通過多進程只用了4秒,速度大大加快了。
另外,全局解釋器鎖存在于Cpython的解釋器中,如果你使用其他的Python解釋器,比如Jython(使用Java編寫), IronPython(使用C#編寫)和PyPy(使用Python編寫),可能不會遇到全局解釋器鎖的情況。
總結
全局解釋器鎖在CPython中,限制了CPU密集型的多線程處理任務。遇到CPU密集型的多線程處理任務時,我們盡量使用多進程的方式來處理(multiprocessing)。
=====》》》《《《《======
擴展:在R、Python和shell中如何實現多進程處理:
資料來源:
總結
以上是生活随笔為你收集整理的c语言的锁和Python锁,Python中全局解释器锁、多线程和多进程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 纪念北京冬奥会一周年 冰墩墩分墩“板镜墩
- 下一篇: 比亚迪获瑞典 52 台电动巴士订单:配备