
前言
另一個例子是在銀行賬戶上:假如要在兩個銀行賬戶之間執行交易,你必須確定兩個賬戶都被鎖定,不受其他交易的影響,以達到正确的資金轉移量。在這裡,這個類比并不完全成立--哲學家對應的是鎖定賬戶的交易(分叉)--但同樣的技術困難也會出現。
其他的例子包括電商秒殺系統,多個使用者搶一個商品,不允許一個資料庫被多個客戶同時修改。
死鎖也是由一個并發程式需要同時具備的條件來定義的,這樣才會發生死鎖。這些條件是由計算機科學家Edward G. Coffman, Jr .首先提出的,是以被稱為 Coffman 條件。這些條件如下:
- 至少有一個資源必須處于不可共享的狀态。這意味着該資源被一個單獨的程序(或線程)持有,不能被其他人通路; 在任何時間内,該資源隻能被單個的程序(或線程)通路和持有。這個條件也被稱為互相排斥。
- 有一個程序(或線程)同時通路一個資源并等待其他程序(或線程)持有的另一個資源。換句話說,這個程序(或線程)需要通路兩個資源來執行其指令,其中一個它已經持有,另一個它正在等待其他程序(或線程)。這種情況被稱為保持和等待。
- 隻有在有特定指令讓程序(或線程)釋放資源的情況下,才能由持有這些資源的程序(或線程)來釋放。這就是說,除非程序(或線程)自願主動地釋放資源,否則該資源仍處于不可共享的狀态。這就是無搶占條件。
- 最後一個條件叫做循環等待。顧名思義,這個條件規定了一組程序(或線程)的存在,是以這組程序中的第一個程序(或線程)正在等待第二個程序(或線程)釋放資源,而第二個程序(或線程)又需要等待第三個程序(或線程);最後,這組程序中的最後一個程序(或線程)正在等待第一個程序。
造成線程死鎖的常見例子包括:
- 一個在自己身上等待的線程(例如,試圖兩次獲得同一個互斥鎖)
- 互相等待的線程(例如,A 等待 B,B 等待 A)
- 未能釋放資源的線程(例如,互斥鎖、信号量、屏障、條件、事件等)
- 線程以不同的順序擷取互斥鎖(例如,未能執行鎖排序)
模拟死鎖1:線程等待本身
導緻死鎖的一個常見原因是線程在自己身上等待。
我們并不打算讓這種死鎖發生,例如,我們不會故意寫代碼,導緻線程自己等待。相反,由于一系列的函數調用和變量的傳遞,這種情況會意外地發生。
一個線程可能會因為很多原因而在自己身上等待,比如:
- 等待獲得它已經獲得的互斥鎖
- 等待自己被通知一個條件
- 等待一個事件被自己設定
- 等待一個信号被自己釋放
開發一個
task()
函數,直接嘗試兩次擷取同一個 mutex 鎖。也就是說,該任務将擷取鎖,然後再次嘗試擷取鎖。
# 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
這将導緻死鎖,因為線程已經持有該鎖,并将永遠等待自己釋放該鎖,以便它能再次獲得該鎖,
task()
試圖兩次擷取同一個鎖并觸發死鎖。
在主線程中,可以建立鎖:
# create the mutex lock
lock = Lock()
然後我們将建立并配置一個新的線程,在一個新的線程中執行我們的
task()
函數,然後啟動這個線程并等待它終止,而它永遠不會終止。
# 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()
運作結果如下:
首先建立鎖,然後新的線程被混淆并啟動,主線程阻塞,直到新線程終止,但它從未這樣做。
新線程運作并首先獲得了鎖。然後它試圖再次獲得相同的互斥鎖并阻塞。
它将永遠阻塞,等待鎖被釋放。該鎖不能被釋放,因為該線程已經持有該鎖。是以,該線程已經陷入死鎖。
該程式必須被強制終止,例如,通過 Control-C 殺死終端。
模拟死鎖2:線程互相等待
一個常見的例子就是兩個或多個線程互相等待。例如:線程 A 等待線程 B,線程 B 等待線程 A。
如果有三個線程,可能會出現線程循環等待,例如:
- 線程 A:等待線程 B
- 線程 B:等待線程 C
- 線程 C:等待線程 A
如果你設定了線程來等待其他線程的結果,這種死鎖是很常見的,比如在一個流水線或工作流中,子任務的一些依賴關系是不符合順序的。
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)
首先得到主線程的執行個體
main_thread
,然後建立一個新的線程
new_thread
,并調用傳遞給主線程的
task()
函數。新線程傳回一條資訊并等待主線程停止,主線程用新線程的執行個體調用
task()
函數,并等待新線程的終止。每個線程都在等待另一個線程終止,然後自己才能終止,這導緻了一個死鎖。
運作結果:
[Thread-1] waiting on [MainThread]...
[MainThread] waiting on [Thread-1]...
模拟死鎖3:以錯誤的順序擷取鎖
導緻死鎖的一個常見原因是,兩個線程同時以不同的順序獲得鎖。例如,我們可能有一個受鎖保護的關鍵部分,在這個關鍵部分中,我們可能有代碼或函數調用受第二個鎖保護。
可能會遇到這樣的情況:一個線程獲得了鎖 1 ,然後試圖獲得鎖 2,然後有第二個線程調用獲得鎖 2 的功能,然後試圖獲得鎖 1。如果這種情況同時發生,線程 1 持有鎖 1,線程 2 持有鎖 2,那麼就會有一個死鎖。
- 線程1: 持有鎖 1, 等待鎖 2
- 線程2 : 持有鎖 2, 等待鎖 1
# example of a deadlock caused by acquiring locks in a different order
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()
運作這個例子首先建立了兩個鎖。然後兩個線程都被建立,主線程等待線程的終止。
第一個線程接收 lock1 和 lock2 作為參數。它獲得了鎖 1 并 sleep。
第二個線程接收 lock2 和 lock1 作為參數。它獲得了鎖 2 并 sleep。
第一個線程醒來并試圖擷取鎖 2,但它必須等待,因為它已經被第二個線程擷取。第二個線程醒來并試圖擷取鎖 1,但它必須等待,因為它已經被第一個線程擷取。
結果是一個死鎖:
Thread 1 acquiring lock 1...
Thread 2 acquiring lock 1...
Thread 1 acquiring lock 2...
Thread 2 acquiring lock 2...
解決辦法是確定鎖在整個程式中總是以相同的順序獲得。這就是所謂的鎖排序。
模拟死鎖4:鎖未釋放
導緻死鎖的另一個常見原因是線程未能釋放一個資源。這通常是由線程在關鍵部分引發錯誤或異常造成的,這種方式會阻止線程釋放資源,包括:
- 未能釋放一個鎖
- 未能釋放一個信号器
- 未能到達一個 barrier
- 未能在一個條件上通知線程
- 未能設定一個事件
# 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()
Thread acquiring lock...
Exception in thread Thread-1:
Traceback (most recent call last):
...
Exception: Something bad happened
Main acquiring lock...