Python?模擬死鎖的常見實(shí)例詳解
前言
常見的例子是在銀行賬戶上:假如要在兩個(gè)銀行賬戶之間執(zhí)行交易,你必須確保兩個(gè)賬戶都被鎖定,不受其他交易的影響,以達(dá)到正確的資金轉(zhuǎn)移量。在這里,這個(gè)類比并不完全成立--哲學(xué)家對應(yīng)的是鎖定賬戶的交易(分叉)--但同樣的技術(shù)困難也會(huì)出現(xiàn)。
其他的例子包括電商秒殺系統(tǒng),多個(gè)用戶搶一個(gè)商品,不允許一個(gè)數(shù)據(jù)庫被多個(gè)客戶同時(shí)修改。
死鎖也是由一個(gè)并發(fā)程序需要同時(shí)具備的條件來定義的,這樣才會(huì)發(fā)生死鎖。這些條件是由計(jì)算機(jī)科學(xué)家Edward G. Coffman, Jr .首先提出的,因此被稱為 Coffman 條件。這些條件如下:
- 至少有一個(gè)資源必須處于不可共享的狀態(tài)。這意味著該資源被一個(gè)單獨(dú)的進(jìn)程(或線程)持有,不能被其他人訪問; 在任何時(shí)間內(nèi),該資源只能被單個(gè)的進(jìn)程(或線程)訪問和持有。這個(gè)條件也被稱為相互排斥。
- 有一個(gè)進(jìn)程(或線程)同時(shí)訪問一個(gè)資源并等待其他進(jìn)程(或線程)持有的另一個(gè)資源。換句話說,這個(gè)進(jìn)程(或線程)需要訪問兩個(gè)資源來執(zhí)行其指令,其中一個(gè)它已經(jīng)持有,另一個(gè)它正在等待其他進(jìn)程(或線程)。這種情況被稱為保持和等待。
- 只有在有特定指令讓進(jìn)程(或線程)釋放資源的情況下,才能由持有這些資源的進(jìn)程(或線程)來釋放。這就是說,除非進(jìn)程(或線程)自愿主動(dòng)地釋放資源,否則該資源仍處于不可共享的狀態(tài)。這就是無搶占條件。
- 最后一個(gè)條件叫做循環(huán)等待。顧名思義,這個(gè)條件規(guī)定了一組進(jìn)程(或線程)的存在,因此這組進(jìn)程中的第一個(gè)進(jìn)程(或線程)正在等待第二個(gè)進(jìn)程(或線程)釋放資源,而第二個(gè)進(jìn)程(或線程)又需要等待第三個(gè)進(jìn)程(或線程);最后,這組進(jìn)程中的最后一個(gè)進(jìn)程(或線程)正在等待第一個(gè)進(jìn)程。
造成線程死鎖的常見例子包括:
- 一個(gè)在自己身上等待的線程(例如,試圖兩次獲得同一個(gè)互斥鎖)
- 互相等待的線程(例如,A 等待 B,B 等待 A)
- 未能釋放資源的線程(例如,互斥鎖、信號量、屏障、條件、事件等)
- 線程以不同的順序獲取互斥鎖(例如,未能執(zhí)行鎖排序)
模擬死鎖1:線程等待本身
導(dǎo)致死鎖的一個(gè)常見原因是線程在自己身上等待。
我們并不打算讓這種死鎖發(fā)生,例如,我們不會(huì)故意寫代碼,導(dǎo)致線程自己等待。相反,由于一系列的函數(shù)調(diào)用和變量的傳遞,這種情況會(huì)意外地發(fā)生。
一個(gè)線程可能會(huì)因?yàn)楹芏嘣蚨谧约荷砩系却?,比如?/p>
- 等待獲得它已經(jīng)獲得的互斥鎖
- 等待自己被通知一個(gè)條件
- 等待一個(gè)事件被自己設(shè)置
- 等待一個(gè)信號被自己釋放
開發(fā)一個(gè) task()
函數(shù),直接嘗試兩次獲取同一個(gè) mutex 鎖。也就是說,該任務(wù)將獲取鎖,然后再次嘗試獲取鎖。
# task to be executed in a new thread def task(lock): print('Thread acquiring lock...') with lock: print('Thread acquiring lock again...') with lock: # will never get here pass
這將導(dǎo)致死鎖,因?yàn)榫€程已經(jīng)持有該鎖,并將永遠(yuǎn)等待自己釋放該鎖,以便它能再次獲得該鎖, task()
試圖兩次獲取同一個(gè)鎖并觸發(fā)死鎖。
在主線程中,可以創(chuàng)建鎖:
# create the mutex lock lock = Lock()
然后我們將創(chuàng)建并配置一個(gè)新的線程,在一個(gè)新的線程中執(zhí)行我們的 task()
函數(shù),然后啟動(dòng)這個(gè)線程并等待它終止,而它永遠(yuǎn)不會(huì)終止。
# create and configure the new thread thread = Thread(target=task, args=(lock,)) # start the new thread thread.start() # wait for threads to exit... thread.join()
完整代碼如下:
from threading import Thread from threading import Lock # task to be executed in a new thread def task(lock): print('Thread acquiring lock...') with lock: print('Thread acquiring lock again...') with lock: # will never get here pass # create the mutex lock lock = Lock() # create and configure the new thread thread = Thread(target=task, args=(lock,)) # start the new thread thread.start() # wait for threads to exit... thread.join()
運(yùn)行結(jié)果如下:
首先創(chuàng)建鎖,然后新的線程被混淆并啟動(dòng),主線程阻塞,直到新線程終止,但它從未這樣做。
新線程運(yùn)行并首先獲得了鎖。然后它試圖再次獲得相同的互斥鎖并阻塞。
它將永遠(yuǎn)阻塞,等待鎖被釋放。該鎖不能被釋放,因?yàn)樵摼€程已經(jīng)持有該鎖。因此,該線程已經(jīng)陷入死鎖。
該程序必須被強(qiáng)制終止,例如,通過 Control-C 殺死終端。
模擬死鎖2:線程互相等待
一個(gè)常見的例子就是兩個(gè)或多個(gè)線程互相等待。例如:線程 A 等待線程 B,線程 B 等待線程 A。
如果有三個(gè)線程,可能會(huì)出現(xiàn)線程循環(huán)等待,例如:
- 線程 A:等待線程 B
- 線程 B:等待線程 C
- 線程 C:等待線程 A
如果你設(shè)置了線程來等待其他線程的結(jié)果,這種死鎖是很常見的,比如在一個(gè)流水線或工作流中,子任務(wù)的一些依賴關(guān)系是不符合順序的。
from threading import current_thread from threading import Thread # task to be executed in a new thread def task(other): # message print(f'[{current_thread().name}] waiting on [{other.name}]...\n') other.join() # get the current thread main_thread = current_thread() # create the second thread new_thread = Thread(target=task, args=(main_thread,)) # start the new thread new_thread.start() # run the first thread task(new_thread)
首先得到主線程的實(shí)例 main_thread
,然后創(chuàng)建一個(gè)新的線程 new_thread
,并調(diào)用傳遞給主線程的 task()
函數(shù)。新線程返回一條信息并等待主線程停止,主線程用新線程的實(shí)例調(diào)用 task()
函數(shù),并等待新線程的終止。每個(gè)線程都在等待另一個(gè)線程終止,然后自己才能終止,這導(dǎo)致了一個(gè)死鎖。
運(yùn)行結(jié)果:
[Thread-1] waiting on [MainThread]...
[MainThread] waiting on [Thread-1]...
模擬死鎖3:以錯(cuò)誤的順序獲取鎖
導(dǎo)致死鎖的一個(gè)常見原因是,兩個(gè)線程同時(shí)以不同的順序獲得鎖。例如,我們可能有一個(gè)受鎖保護(hù)的關(guān)鍵部分,在這個(gè)關(guān)鍵部分中,我們可能有代碼或函數(shù)調(diào)用受第二個(gè)鎖保護(hù)。
可能會(huì)遇到這樣的情況:一個(gè)線程獲得了鎖 1 ,然后試圖獲得鎖 2,然后有第二個(gè)線程調(diào)用獲得鎖 2 的功能,然后試圖獲得鎖 1。如果這種情況同時(shí)發(fā)生,線程 1 持有鎖 1,線程 2 持有鎖 2,那么就會(huì)有一個(gè)死鎖。
- 線程1: 持有鎖 1, 等待鎖 2
- 線程2 : 持有鎖 2, 等待鎖 1
from time import sleep from threading import Thread from threading import Lock # task to be executed in a new thread def task(number, lock1, lock2): # acquire the first lock print(f'Thread {number} acquiring lock 1...') with lock1: # wait a moment sleep(1) # acquire the next lock print(f'Thread {number} acquiring lock 2...') with lock2: # never gets here.. pass # create the mutex locks lock1 = Lock() lock2 = Lock() # create and configure the new threads thread1 = Thread(target=task, args=(1, lock1, lock2)) thread2 = Thread(target=task, args=(2, lock2, lock1)) # start the new threads thread1.start() thread2.start() # wait for threads to exit... thread1.join() thread2.join()
運(yùn)行這個(gè)例子首先創(chuàng)建了兩個(gè)鎖。然后兩個(gè)線程都被創(chuàng)建,主線程等待線程的終止。
第一個(gè)線程接收 lock1 和 lock2 作為參數(shù)。它獲得了鎖 1 并 sleep。
第二個(gè)線程接收 lock2 和 lock1 作為參數(shù)。它獲得了鎖 2 并 sleep。
第一個(gè)線程醒來并試圖獲取鎖 2,但它必須等待,因?yàn)樗呀?jīng)被第二個(gè)線程獲取。第二個(gè)線程醒來并試圖獲取鎖 1,但它必須等待,因?yàn)樗呀?jīng)被第一個(gè)線程獲取。
結(jié)果是一個(gè)死鎖:
Thread 1 acquiring lock 1...
Thread 2 acquiring lock 1...
Thread 1 acquiring lock 2...
Thread 2 acquiring lock 2...
解決辦法是確保鎖在整個(gè)程序中總是以相同的順序獲得。這就是所謂的鎖排序。
模擬死鎖4:鎖未釋放
導(dǎo)致死鎖的另一個(gè)常見原因是線程未能釋放一個(gè)資源。這通常是由線程在關(guān)鍵部分引發(fā)錯(cuò)誤或異常造成的,這種方式會(huì)阻止線程釋放資源,包括:
- 未能釋放一個(gè)鎖
- 未能釋放一個(gè)信號器
- 未能到達(dá)一個(gè) barrier
- 未能在一個(gè)條件上通知線程
- 未能設(shè)置一個(gè)事件
# example of a deadlock caused by a thread failing to release a lock from time import sleep from threading import Thread from threading import Lock # task to be executed in a new thread def task(lock): # acquire the lock print('Thread acquiring lock...') lock.acquire() # fail raise Exception('Something bad happened') # release the lock (never gets here) print('Thread releasing lock...') lock.release() # create the mutex lock lock = Lock() # create and configure the new thread thread = Thread(target=task, args=(lock,)) # start the new thread thread.start() # wait a while sleep(1) # acquire the lock print('Main acquiring lock...') lock.acquire() # do something... # release lock (never gets here) lock.release()
運(yùn)行該例子時(shí),首先創(chuàng)建鎖,然后創(chuàng)建并啟動(dòng)新的線程。然后主線程阻塞。新線程運(yùn)行。它首先獲得了鎖,然后引發(fā)了一個(gè)異常而失敗。該線程解開了鎖,但卻沒有解開鎖的代碼。新的線程終止了。最后,主線程被喚醒,然后試圖獲取鎖。由于鎖沒有被釋放,主線程永遠(yuǎn)阻塞,導(dǎo)致了死鎖。
Thread acquiring lock...
Exception in thread Thread-1:
Traceback (most recent call last):
...
Exception: Something bad happened
Main acquiring lock...
總結(jié)
本文首先通過實(shí)際案例中可能出現(xiàn)死鎖的情況,介紹了死鎖的概念及條件,并通過 Python 代碼模擬死鎖的四種情況,更多關(guān)于Python 模擬死鎖的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
scrapy實(shí)踐之翻頁爬取的實(shí)現(xiàn)
這篇文章主要介紹了scrapy實(shí)踐之翻頁爬取的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01解決Python3.8用pip安裝turtle-0.0.2出現(xiàn)錯(cuò)誤問題
turtle庫是python的基礎(chǔ)繪圖庫,這個(gè)庫被介紹為一個(gè)最常用的用來給孩子們介紹編程知識的方法庫,這篇文章主要介紹了解決Python3.8用pip安裝turtle-0.0.2出現(xiàn)錯(cuò)誤問題,需要的朋友可以參考下2020-02-02Python+OpenCV 實(shí)現(xiàn)簡單的高斯濾波(推薦)
這篇文章主要介紹了Python+OpenCV 實(shí)現(xiàn)簡單的高斯濾波,在文中需要注意的是,這里我沒有特判當(dāng)sigma = 0的時(shí)候的情況,具體實(shí)現(xiàn)過程跟隨小編一起看看吧2021-09-09Python給exe添加以管理員運(yùn)行的屬性方法詳解
這篇文章主要為大家介紹了Python給exe添加以管理員運(yùn)行的屬性方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12解決pytorch trainloader遇到的多進(jìn)程問題
這篇文章主要介紹了解決pytorch trainloader遇到的多進(jìn)程問題,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-05-05