問題
你正在寫一個多線程程序,其中線程需要一次獲取多個鎖,此時如何避免死鎖問題。
解決方案
在多線程程序中,死鎖問題很大一部分是由于線程同時獲取多個鎖造成的。舉個例子:一個線程獲取了第一個鎖,然后在獲取第二個鎖的 時候發生阻塞,那么這個線程就可能阻塞其他線程的執行,從而導致整個程序假死。 解決死鎖問題的一種方案是為程序中的每一個鎖分配一個唯一的id,然后只允許按照升序規則來使用多個鎖,這個規則使用上下文管理器 是非常容易實現的,示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
import threading from contextlib import contextmanager # Thread-local state to stored information on locks already acquired _local = threading.local() @contextmanager def acquire( * locks): # Sort locks by object identifier locks = sorted (locks, key = lambda x: id (x)) # Make sure lock order of previously acquired locks is not violated acquired = getattr (_local, 'acquired' ,[]) if acquired and max ( id (lock) for lock in acquired) > = id (locks[ 0 ]): raise RuntimeError( 'Lock Order Violation' ) # Acquire all of the locks acquired.extend(locks) _local.acquired = acquired try : for lock in locks: lock.acquire() yield finally : # Release locks in reverse order of acquisition for lock in reversed (locks): lock.release() del acquired[ - len (locks):] |
如何使用這個上下文管理器呢?你可以按照正常途徑創建一個鎖對象,但不論是單個鎖還是多個鎖中都使用 acquire()
函數來申請鎖, 示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
import threading x_lock = threading.Lock() y_lock = threading.Lock() def thread_1(): while True : with acquire(x_lock, y_lock): print ( 'Thread-1' ) def thread_2(): while True : with acquire(y_lock, x_lock): print ( 'Thread-2' ) t1 = threading.Thread(target = thread_1) t1.daemon = True t1.start() t2 = threading.Thread(target = thread_2) t2.daemon = True t2.start() |
如果你執行這段代碼,你會發現它即使在不同的函數中以不同的順序獲取鎖也沒有發生死鎖。 其關鍵在于,在第一段代碼中,我們對這些鎖進行了排序。通過排序,使得不管用戶以什么樣的順序來請求鎖,這些鎖都會按照固定的順序被獲取。 如果有多個 acquire() 操作被嵌套調用,可以通過線程本地存儲(TLS)來檢測潛在的死鎖問題。 假設你的代碼是這樣寫的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
import threading x_lock = threading.Lock() y_lock = threading.Lock() def thread_1(): while True : with acquire(x_lock): with acquire(y_lock): print ( 'Thread-1' ) def thread_2(): while True : with acquire(y_lock): with acquire(x_lock): print ( 'Thread-2' ) t1 = threading.Thread(target = thread_1) t1.daemon = True t1.start() t2 = threading.Thread(target = thread_2) t2.daemon = True t2.start() |
如果你運行這個版本的代碼,必定會有一個線程發生崩潰,異常信息可能像這樣:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
Exception in thread Thread - 1 : Traceback (most recent call last): File "/usr/local/lib/python3.3/threading.py" , line 639 , in _bootstrap_inner self .run() File "/usr/local/lib/python3.3/threading.py" , line 596 , in run self ._target( * self ._args, * * self ._kwargs) File "deadlock.py" , line 49 , in thread_1 with acquire(y_lock): File "/usr/local/lib/python3.3/contextlib.py" , line 48 , in __enter__ return next ( self .gen) File "deadlock.py" , line 15 , in acquire raise RuntimeError( "Lock Order Violation" ) RuntimeError: Lock Order Violation >>> |
發生崩潰的原因在于,每個線程都記錄著自己已經獲取到的鎖。 acquire() 函數會檢查之前已經獲取的鎖列表, 由于鎖是按照升序排列獲取的,所以函數會認為之前已獲取的鎖的id必定小于新申請到的鎖,這時就會觸發異常。
討論
死鎖是每一個多線程程序都會面臨的一個問題(就像它是每一本操作系統課本的共同話題一樣)。根據經驗來講,盡可能保證每一個 線程只能同時保持一個鎖,這樣程序就不會被死鎖問題所困擾。一旦有線程同時申請多個鎖,一切就不可預料了。
死鎖的檢測與恢復是一個幾乎沒有優雅的解決方案的擴展話題。一個比較常用的死鎖檢測與恢復的方案是引入看門狗計數器。當線程正常 運行的時候會每隔一段時間重置計數器,在沒有發生死鎖的情況下,一切都正常進行。一旦發生死鎖,由于無法重置計數器導致定時器 超時,這時程序會通過重啟自身恢復到正常狀態。
避免死鎖是另外一種解決死鎖問題的方式,在進程獲取鎖的時候會嚴格按照對象id升序排列獲取,經過數學證明,這樣保證程序不會進入 死鎖狀態。證明就留給讀者作為練習了。避免死鎖的主要思想是,單純地按照對象id遞增的順序加鎖不會產生循環依賴,而循環依賴是 死鎖的一個必要條件,從而避免程序進入死鎖狀態。
下面以一個關于線程死鎖的經典問題:“哲學家就餐問題”,作為本節最后一個例子。題目是這樣的:五位哲學家圍坐在一張桌子前,每個人 面前有一碗飯和一只筷子。在這里每個哲學家可以看做是一個獨立的線程,而每只筷子可以看做是一個鎖。每個哲學家可以處在靜坐、 思考、吃飯三種狀態中的一個。需要注意的是,每個哲學家吃飯是需要兩只筷子的,這樣問題就來了:如果每個哲學家都拿起自己左邊的筷子, 那么他們五個都只能拿著一只筷子坐在那兒,直到餓死。此時他們就進入了死鎖狀態。 下面是一個簡單的使用死鎖避免機制解決“哲學家就餐問題”的實現:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
import threading # The philosopher thread def philosopher(left, right): while True : with acquire(left,right): print (threading.currentThread(), 'eating' ) # The chopsticks (represented by locks) NSTICKS = 5 chopsticks = [threading.Lock() for n in range (NSTICKS)] # Create all of the philosophers for n in range (NSTICKS): t = threading.Thread(target = philosopher, args = (chopsticks[n],chopsticks[(n + 1 ) % NSTICKS])) t.start() |
最后,要特別注意到,為了避免死鎖,所有的加鎖操作必須使用 acquire()
函數。如果代碼中的某部分繞過acquire 函數直接申請鎖,那么整個死鎖避免機制就不起作用了。
以上就是Python 防止死鎖的方法的詳細內容,更多關于Python 防止死鎖的資料請關注服務器之家其它相關文章!
原文鏈接:https://python3-cookbook.readthedocs.io/zh_CN/latest/c12/p05_locking_with_deadlock_avoidance.html