Python 多線程死鎖問題的巧妙解決方法
死鎖
死鎖的原理非常簡單,用一句話就可以描述完。就是當(dāng)多線程訪問多個鎖的時候,不同的鎖被不同的線程持有,它們都在等待其他線程釋放出鎖來,于是便陷入了永久等待。比如A線程持有1號鎖,等待2號鎖,B線程持有2號鎖等待1號鎖,那么它們永遠(yuǎn)也等不到執(zhí)行的那天,這種情況就叫做死鎖。
關(guān)于死鎖有一個著名的問題叫做哲學(xué)家就餐問題,有5個哲學(xué)家圍坐在一起,他們每個人需要拿到兩個叉子才可以吃飯。如果他們同時拿起自己左手邊的叉子,那么就會永遠(yuǎn)等待右手邊的叉子釋放出來。這樣就陷入了永久等待,于是這些哲學(xué)家都會餓死。
img
這是一個很形象的模型,因為在計算機(jī)并發(fā)場景當(dāng)中,一些資源的數(shù)量往往是有限的。很有可能出現(xiàn)多個線程搶占的情況,如果處理不好就會發(fā)生大家都獲取了一個資源,然后在等待另外的資源的情況。
對于死鎖的問題有多種解決方法,這里我們介紹比較簡單的一種,就是對這些鎖進(jìn)行編號。我們規(guī)定當(dāng)一個線程需要同時持有多個鎖的時候,必須要按照序號升序的順序?qū)@些鎖進(jìn)行訪問。通過上下文管理器我們可以很容易實現(xiàn)這一點。
上下文管理器
首先我們來簡單介紹一下上下文管理器,上下文管理器我們其實經(jīng)常使用,比如我們經(jīng)常使用的with語句就是一個上下文管理器的經(jīng)典使用。當(dāng)我們通過with語句打開文件的時候,它會自動替我們處理好文件讀取之后的關(guān)閉以及拋出異常的處理,可以節(jié)約我們大量的代碼。
同樣我們也可以自己定義一個上下文處理器,其實很簡單,我們只需要實現(xiàn)__enter__和__exit__這兩個函數(shù)即可。__enter__函數(shù)用來實現(xiàn)進(jìn)入資源之前的操作和處理,那么顯然__exit__函數(shù)對應(yīng)的就是使用資源結(jié)束之后或者是出現(xiàn)異常的處理邏輯。有了這兩個函數(shù)之后,我們就有了自己的上下文處理類了。
我們來看一個樣例:
classSample:
def__enter__(self):
print('enterresources')
returnself
def__exit__(self,exc_type,exc_val,exc_tb):
print('exit')
#print(exc_type)
#print(exc_val)
#print(exc_tb)
defdoSomething(self):
a=1/1
returna
defgetSample():
returnSample()
if__name__=='__main__':
withgetSample()assample:
print('dosomething')
sample.doSomething()
當(dāng)我們運行這段代碼的時候,屏幕上打印的結(jié)果和我們的預(yù)期是一致的。
我們觀察一下__exit__函數(shù),會發(fā)現(xiàn)它的參數(shù)有4個,后面的三個參數(shù)對應(yīng)的是拋出異常的情況。type對應(yīng)異常的類型,val對應(yīng)異常時的輸出值,trace對應(yīng)異常拋出時的運行堆棧。這些信息都是我們排查異常的時候經(jīng)常需要用到的信息,通過這三個字段,我們可以根據(jù)我們的需要對可能出現(xiàn)的異常進(jìn)行自定義的處理。
實現(xiàn)上下文管理器并不一定要通過類實現(xiàn),Python當(dāng)中也提供了上下文管理的注解,通過使用注解我們可以很方便地實現(xiàn)上下文管理。我們同樣也來看一個例子:
importtime
fromcontextlibimportcontextmanager
@contextmanager
deftimethis(label):
start=time.time()
try:
yield
finally:
end=time.time()
print('{}:{}'.format(label,end-start))
withtimethis('timer'):
pass
在這個方法當(dāng)中yield之前的部分相當(dāng)于__enter__函數(shù),yield之后的部分相當(dāng)于__exit__。如果出現(xiàn)異常會在try語句當(dāng)中拋出,那么我們編寫except對異常進(jìn)行處理即可。
避免死鎖
了解了上下文管理器之后,我們要做的就是在lock的外面包裝一層,使得我們在獲取和釋放鎖的時候可以根據(jù)我們的需要,對鎖進(jìn)行排序,按照升序的順序進(jìn)行持有。
這段代碼源于Python的著名進(jìn)階書籍《Pythoncookbook》,非常經(jīng)典:
fromcontextlibimportcontextmanager
#用來存儲local的數(shù)據(jù)
_local=threading.local()
@contextmanager
defacquire(*locks):
#對鎖按照id進(jìn)行排序
locks=sorted(locks,key=lambdax:id(x))
#如果已經(jīng)持有鎖當(dāng)中的序號有比當(dāng)前更大的,說明策略失敗
acquired=getattr(_local,'acquired',[])
ifacquiredandmax(id(lock)forlockinacquired)>=id(locks[0]):
raiseRuntimeError('LockOrderViolation')
#獲取所有鎖
acquired.extend(locks)
_local.acquired=acquired
try:
forlockinlocks:
lock.acquire()
yield
finally:
#倒敘釋放
forlockinreversed(locks):
lock.release()
delacquired[-len(locks):]
這段代碼寫得非常漂亮,可讀性很高,邏輯我們都應(yīng)該能看懂,但是有一個小問題是這里用到了threading.local這個組件。
它是一個多線程場景當(dāng)中的共享變量,雖然說是共享的,但是對于每個線程來說讀取到的值都是獨立的。聽起來有些難以理解,其實我們可以將它理解成一個dict,dict的key是每一個線程的id,value是一個存儲數(shù)據(jù)的dict。每個線程在訪問local變量的時候,都相當(dāng)于先通過線程id獲取了一個獨立的dict,再對這個dict進(jìn)行的操作。
看起來我們在使用的時候直接使用了_local,這是因為通過線程id先進(jìn)行查詢的步驟在其中封裝了。不明就里的話可能會覺得有些難以理解。
我們再來看下這個acquire的使用:
x_lock=threading.Lock()
y_lock=threading.Lock()
defthread_1():
whileTrue:
withacquire(x_lock,y_lock):
print('Thread-1')
defthread_2():
whileTrue:
withacquire(y_lock,x_lock):
print('Thread-2')
t1=threading.Thread(target=thread_1)
t1.start()
t2=threading.Thread(target=thread_2)
t2.start()
運行一下會發(fā)現(xiàn)沒有出現(xiàn)死鎖的情況,但如果我們把代碼稍加調(diào)整,寫成這樣,那么就會觸發(fā)異常了。
defthread_1():
whileTrue:
withacquire(x_lock):
withacquire(y_lock):
print('Thread-1')
defthread_2():
whileTrue:
withacquire(y_lock):
withacquire(x_lock):
print('Thread-1')
因為我們把鎖寫成了層次結(jié)構(gòu),這樣就沒辦法進(jìn)行排序保證持有的有序性了,那么就會觸發(fā)我們代碼當(dāng)中定義的異常。
最后我們再來看下哲學(xué)家就餐問題,通過我們自己實現(xiàn)的acquire函數(shù)我們可以非常方便地解決他們死鎖吃不了飯的問題。
importthreading
defphilosopher(left,right):
whileTrue:
withacquire(left,right):
print(threading.currentThread(),'eating')
#叉子的數(shù)量
NSTICKS=5
chopsticks=[threading.Lock()forninrange(NSTICKS)]
forninrange(NSTICKS):
t=threading.Thread(target=philosopher,
args=(chopsticks[n],chopsticks[(n+1)%NSTICKS]))
t.start()
總結(jié)
關(guān)于死鎖的問題,對鎖進(jìn)行排序只是其中的一種解決方案,除此之外還有很多解決死鎖的模型。比如我們可以讓線程在嘗試持有新的鎖失敗的時候主動放棄所有目前已經(jīng)持有的鎖,比如我們可以設(shè)置機(jī)制檢測死鎖的發(fā)生并對其進(jìn)行處理等等。發(fā)散出去其實有很多種方法,這些方法起作用的原理各不相同,其中涉及大量操作系統(tǒng)的基礎(chǔ)概念和知識,感興趣的同學(xué)可以深入研究一下這個部分,一定會對操作系統(tǒng)以及鎖的使用有一個深刻的認(rèn)識。
以上內(nèi)容為大家介紹了Python多線程死鎖問題的巧妙解決方法,希望對大家有所幫助,如果想要了解更多Python相關(guān)知識,請關(guān)注IT培訓(xùn)機(jī)構(gòu):千鋒教育。

猜你喜歡LIKE
相關(guān)推薦HOT
更多>>
pythonfor循環(huán)是什么
pythonfor循環(huán)是什么在做遍歷的時候,對于一些數(shù)據(jù)的反復(fù)循環(huán)執(zhí)行,我們會用到for循環(huán)的語句。可以說這是新手入門必學(xué)的語句之一,在很多基礎(chǔ)循...詳情>>
2023-11-13 07:46:36
pythoncontextmanager()的轉(zhuǎn)換
python中contextmanager()的轉(zhuǎn)換1、說明當(dāng)發(fā)出請求時,requests庫會在將請求實際發(fā)送到目標(biāo)服務(wù)器之前準(zhǔn)備該請求。請求準(zhǔn)備包括像驗證頭信息和...詳情>>
2023-11-13 06:34:35
python使用items()遍歷鍵值對
python使用items()遍歷鍵值對字典可以用來存儲各種方式的信息,所以有很多方式可以通過字典的所有鍵值對、鍵或值。說明1、即使通過字典,鍵值對...詳情>>
2023-11-13 04:24:15
python實例方法中self的作用
python實例方法中self的作用說明1、無論是創(chuàng)建類的構(gòu)造方法還是實例方法,最少要包含一個參數(shù)self。2、通過實例的self參數(shù)與對象進(jìn)行綁定,程序...詳情>>
2023-11-13 03:46:48熱門推薦
python實現(xiàn)WSGI的框架
沸pythonfor循環(huán)是什么
熱python-=是什么意思
熱python打開文本文件有哪些方法?
新pythoncontextmanager()的轉(zhuǎn)換
pythonre是什么?
pythondecimal是什么
python列表追加元素出錯的解決
python使用loguru操作日志
python使用items()遍歷鍵值對
pythonvim中有哪些對象
python實例方法中self的作用
pythonin和is的區(qū)分
pythonos.path.join()函數(shù)的使用
技術(shù)干貨







快速通道 更多>>
-
課程介紹
點擊獲取大綱 -
就業(yè)前景
查看就業(yè)薪資 -
學(xué)習(xí)費用
了解課程價格 -
優(yōu)惠活動
領(lǐng)取優(yōu)惠券 -
學(xué)習(xí)資源
領(lǐng)3000G教程 -
師資團(tuán)隊
了解師資團(tuán)隊 -
實戰(zhàn)項目
獲取項目源碼 -
開班地區(qū)
查看來校路線