12.5 防止死鎖的加鎖機制

問題

你正在寫一個多線程程序,其中線程需要一次獲取多個鎖,此時如何避免死鎖問題。

解決方案

在多線程程序中,死鎖問題很大一部分是由於線程同時獲取多個鎖造成的。舉個例子:一個線程獲取了第一個鎖,然後在獲取第二個鎖的 時候發生阻塞,那麼這個線程就可能阻塞其他線程的執行,從而導致整個程序假死。 解決死鎖問題的一種方案是爲程序中的每一個鎖分配一個唯一的id,然後只允許按照升序規則來使用多個鎖,這個規則使用上下文管理器 是非常容易實現的,示例如下:

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))

<span class="c1"># Make sure lock order of previously acquired locks is not violated</span>
<span class="n">acquired</span> <span class="o">=</span> <span class="nb">getattr</span><span class="p">(</span><span class="n">_local</span><span class="p">,</span><span class="s1">'acquired'</span><span class="p">,[])</span>
<span class="k">if</span> <span class="n">acquired</span> <span class="ow">and</span> <span class="nb">max</span><span class="p">(</span><span class="nb">id</span><span class="p">(</span><span class="n">lock</span><span class="p">)</span> <span class="k">for</span> <span class="n">lock</span> <span class="ow">in</span> <span class="n">acquired</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="nb">id</span><span class="p">(</span><span class="n">locks</span><span class="p">[</span><span class="mi">0</span><span class="p">]):</span>
    <span class="k">raise</span> <span class="ne">RuntimeError</span><span class="p">(</span><span class="s1">'Lock Order Violation'</span><span class="p">)</span>

<span class="c1"># Acquire all of the locks</span>
<span class="n">acquired</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">locks</span><span class="p">)</span>
<span class="n">_local</span><span class="o">.</span><span class="n">acquired</span> <span class="o">=</span> <span class="n">acquired</span>

<span class="k">try</span><span class="p">:</span>
    <span class="k">for</span> <span class="n">lock</span> <span class="ow">in</span> <span class="n">locks</span><span class="p">:</span>
        <span class="n">lock</span><span class="o">.</span><span class="n">acquire</span><span class="p">()</span>
    <span class="k">yield</span>
<span class="k">finally</span><span class="p">:</span>
    <span class="c1"># Release locks in reverse order of acquisition</span>
    <span class="k">for</span> <span class="n">lock</span> <span class="ow">in</span> <span class="nb">reversed</span><span class="p">(</span><span class="n">locks</span><span class="p">):</span>
        <span class="n">lock</span><span class="o">.</span><span class="n">release</span><span class="p">()</span>
    <span class="k">del</span> <span class="n">acquired</span><span class="p">[</span><span class="o">-</span><span class="nb">len</span><span class="p">(</span><span class="n">locks</span><span class="p">):]</span>

如何使用這個上下文管理器呢?你可以按照正常途徑創建一個鎖對象,但不論是單個鎖還是多個鎖中都使用 acquire() 函數來申請鎖, 示例如下:

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)來檢測潛在的死鎖問題。 假設你的代碼是這樣寫的:

import threading
x_lock = threading.Lock()
y_lock = threading.Lock()

def thread_1():

<span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
    <span class="k">with</span> <span class="n">acquire</span><span class="p">(</span><span class="n">x_lock</span><span class="p">):</span>
        <span class="k">with</span> <span class="n">acquire</span><span class="p">(</span><span class="n">y_lock</span><span class="p">):</span>
            <span class="k">print</span><span class="p">(</span><span class="s1">'Thread-1'</span><span class="p">)</span>

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()

如果你運行這個版本的代碼,必定會有一個線程發生崩潰,異常信息可能像這樣:

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遞增的順序加鎖不會產生循環依賴,而循環依賴是 死鎖的一個必要條件,從而避免程序進入死鎖狀態。

下面以一個關於線程死鎖的經典問題:“哲學家就餐問題”,作爲本節最後一個例子。題目是這樣的:五位哲學家圍坐在一張桌子前,每個人 面前有一碗飯和一隻筷子。在這裏每個哲學家可以看做是一個獨立的線程,而每隻筷子可以看做是一個鎖。每個哲學家可以處在靜坐、 思考、喫飯三種狀態中的一個。需要注意的是,每個哲學家喫飯是需要兩隻筷子的,這樣問題就來了:如果每個哲學家都拿起自己左邊的筷子, 那麼他們五個都只能拿着一隻筷子坐在那兒,直到餓死。此時他們就進入了死鎖狀態。 下面是一個簡單的使用死鎖避免機制解決“哲學家就餐問題”的實現:

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 函數直接申請鎖,那麼整個死鎖避免機制就不起作用了。

       </div>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章