Python多線程之死鎖


 

 

1.什么是死鎖?

死鎖是由於兩個或以上的線程互相持有對方需要的資源,且都不釋放占有的資源,導致這些線程處於等待狀態,程序無法執行。

2.產生死鎖的四個必要條件

   1.互斥性:線程對資源的占有是排他性的,一個資源只能被一個線程占有,直到釋放。

   2.請求和保持條件:一個線程對請求被占有資源發生阻塞時,對已經獲得的資源不釋放。

   3.不剝奪:一個線程在釋放資源之前,其他的線程無法剝奪占用。

   4.循環等待:發生死鎖時,線程進入死循環,永久阻塞。

3.產生死鎖的原因

在多線程的場景,比如線程A持有獨占鎖資源a,並嘗試去獲取獨占鎖資源b同時,線程B持有獨占鎖資源b,並嘗試去獲取獨占鎖資源a。
這樣線程A和線程B相互持有對方需要的鎖,從而發生阻塞,最終變為死鎖。

造成死鎖的原因可以概括成三句話:
1.不同線程同時占用自己鎖資源
2.這些線程都需要對方的鎖資源
3.這些線程都不放棄自己擁有的資源

線程A持有鎖資源a的同時,線程B也持有了鎖資源b。
線程A想要繼續執行需要鎖資源b,線程B想要繼續執行需要鎖資源a
線程A不釋放鎖資源a,線程B不釋放鎖資源b
線程A線程B都需要a,b兩把鎖,如果我們加鎖的順序一致(線程A先拿a加鎖,再拿b加鎖,再解鎖b,然后解鎖a,線程B同理),就不會出現死鎖的情況。

 

4.三種典型的死鎖

常見的3種死鎖的類型:靜態的鎖順序死鎖動態的鎖順序死鎖協作對象之間的死鎖

靜態的鎖順序死鎖

a和b兩個方法都需要獲得A鎖和B鎖。一個線程執行a方法且已經獲得了A鎖,在等待B鎖;另一個線程執行了b方法且已經獲得了B鎖,在等待A鎖。這種狀態,就是發生了靜態的鎖順序死鎖。

靜態是指,在程序中,對於某個鎖來說加鎖和解鎖的位置是不變的。

我們用Python直觀的演示一下靜態的鎖順序死鎖。
假設銀行系統中,用戶a試圖轉賬100塊給用戶b,與此同時用戶b試圖轉賬200塊給用戶a,則可能產生死鎖。
2個線程互相等待對方的鎖,互相占用着資源不釋放。

# coding=utf-8
import time
import threading


class Account:
    def __init__(self, _id, balance):
        self.id = _id
        self.balance = balance
    def withdraw(self, amount):
        self.balance -= amount
    def deposit(self, amount):
        self.balance += amount


def transfera_b(_from, to, amount):
    lock_a.acquire()  # 鎖住自己的賬戶
    time.sleep(1)  # 讓交易時間變長,2個交易線程時間上重疊,有足夠時間來產生死鎖
    _from.withdraw(amount)
    print('wait for lock_b')
    lock_b.acquire()  # 鎖住對方的賬戶
    to.deposit(amount)
    lock_b.release()
    lock_a.release()


def transferb_a(_from, to, amount):
    lock_b.acquire()  # 鎖住自己的賬戶
    time.sleep(1)  # 讓交易時間變長,2個交易線程時間上重疊,有足夠時間來產生死鎖
    _from.withdraw(amount)
    print('wait for lock_a')
    lock_a.acquire()  # 鎖住對方的賬戶
    to.deposit(amount)
    lock_a.release()
    lock_b.release()

lock_a = threading.Lock()
lock_b = threading.Lock()
a = Account('a', 1000)
b = Account('b', 1000)
#a往b轉賬100
t1 = threading.Thread(target=transfera_b, args=(a, b, 100))
t1.start()
#b往a轉賬200
t2 = threading.Thread(target=transferb_a, args=(b, a, 200))
t2.start()
t1.join()
t2.join()
print("a的賬戶余額:",a.balance)
print("b的賬戶余額:",b.balance)

 

動態的鎖順序死鎖

動態的鎖順序死鎖是指兩個線程調用同一個方法時,傳入的參數顛倒造成的死鎖。如下代碼,一個線程調用了transfer方法並傳入參數a,b,100;另一個線程調用了transfer方法並傳入參數b,a,200。此時就可能發生在靜態的鎖順序死鎖中存在的問題,即:第一個線程獲得了a的鎖並等待b的鎖,第二個線程獲得了b的鎖鎖並等待a的鎖。
這里動態是指,某個鎖會根據參數的傳遞,在不同的位置加鎖和解鎖。
我們這里還是轉賬的例子:
# coding=utf-8
import time
import threading

class Account:
    def __init__(self, _id, balance):
        self.id = _id
        self.balance = balance
        self.lock = threading.Lock()
    def withdraw(self, amount):
        self.balance -= amount
    def deposit(self, amount):
        self.balance += amount

def transfer(_from,to, amount):
    _from.lock.acquire()  # 鎖住自己的賬戶
    time.sleep(1)  # 讓交易時間變長,2個交易線程時間上重疊,有足夠時間來產生死鎖
    _from.withdraw(amount)
    print('wait for lock')
    to.lock.acquire()  # 鎖住對方的賬戶
    to.deposit(amount)
    to.lock.release()
    _from.lock.release()

a = Account('a', 1000)
b = Account('b', 1000)
#a往b轉賬100
t1 = threading.Thread(target=transfer, args=(a, b, 100))
t1.start()
#b往a轉賬200
t2 = threading.Thread(target=transfer, args=(b, a, 200))
t2.start()
t1.join()
t2.join()
print("a的賬戶余額:",a.balance)
print("b的賬戶余額:",b.balance)

 

協作對象之間的死鎖

如果在持有鎖時調用某個外部方法,那么將可能出現死鎖問題。在這個外部方法中可能會獲得其他鎖,或者阻塞時間過長,導致其他線程無法及時獲得當前被持有的鎖。

為了避免這種危險的情況發生,我們使用開放調用。如果調用某個外部方法時不需要持有鎖,我們稱之為開放調用。

 

5.避免死鎖的方法(重點)

避免死鎖可以概括成三種方法:

鎖順序操作的死鎖:

解決靜態的鎖順序死鎖的方法:所有需要多個鎖的線程,都要以相同的順序來獲得鎖。

# coding=utf-8
import time
import threading


class Account:
    def __init__(self, _id, balance):
        self.id = _id
        self.balance = balance
    def withdraw(self, amount):
        self.balance -= amount
    def deposit(self, amount):
        self.balance += amount


def transfera_b(_from, to, amount):
    lock_a.acquire()  # 鎖住自己的賬戶
    time.sleep(1)  # 讓交易時間變長,2個交易線程時間上重疊,有足夠時間來產生死鎖
    _from.withdraw(amount)
    lock_b.acquire()  # 鎖住對方的賬戶
    to.deposit(amount)
    lock_b.release()
    lock_a.release()


def transferb_a(_from, to, amount):
    lock_a.acquire()  # 鎖住自己的賬戶
    time.sleep(1)  # 讓交易時間變長,2個交易線程時間上重疊,有足夠時間來產生死鎖
    _from.withdraw(amount)
    lock_b.acquire()  # 鎖住對方的賬戶
    to.deposit(amount)
    lock_b.release()
    lock_a.release()

lock_a = threading.Lock()
lock_b = threading.Lock()
a = Account('a', 1000)
b = Account('b', 1000)
#a往b轉賬100
t1 = threading.Thread(target=transfera_b, args=(a, b, 100))
t1.start()
#b往a轉賬200
t2 = threading.Thread(target=transferb_a, args=(b, a, 200))
t2.start()
t1.join()
t2.join()
print("a的賬戶余額:",a.balance)
print("b的賬戶余額:",b.balance)
View Code

解決動態的鎖順序死鎖的方法:比較傳入鎖對象的哈希值,根據哈希值的大小來確保所有的線程都以相同的順序獲得鎖 。

# coding=utf-8
import threading
import hashlib
class Account:
    def __init__(self, _id, balance):
        self.id = _id
        self.balance = balance
        self.lock = threading.Lock()
    def withdraw(self, amount):
        self.balance -= amount
    def deposit(self, amount):
        self.balance += amount

def transfer(_from, to, amount):
    hasha,hashb = hashlock(_from, to)
    if hasha >hashb:
        _from.lock.acquire()  # 鎖住自己的賬戶
        to.lock.acquire()  # 鎖住對方的賬戶
        #交易#################
        _from.withdraw(amount)
        to.deposit(amount)
        #################
        to.lock.release()
        _from.lock.release()
    elif hasha < hashb:
        to.lock.acquire()  # 鎖住自己的賬戶
        _from.lock.acquire()  # 鎖住對方的賬戶
        # 交易#################
        _from.withdraw(amount)
        to.deposit(amount)
        #################
        _from.lock.release()
        to.lock.release()
    else: ##hash值相等,最上層使用mylock鎖,你可以把transfer做成一個類,此類中實例一個mylock。
        mylock.acquire()
        _from.lock.acquire()  # 鎖住自己的賬戶
        to.lock.acquire()  # 鎖住對方的賬戶
        # 交易#################
        _from.withdraw(amount)
        to.deposit(amount)
        #################
        to.lock.release()
        _from.lock.release()
        mylock.release()

def hashlock(_from,to):
    hash1 = hashlib.md5()
    hash1.update(bytes(_from.id, encoding='utf-8'))
    hasha = hash1.hexdigest()
    hash = hashlib.md5()
    hash.update(bytes(to.id, encoding='utf-8'))
    hashb = hash.hexdigest()
    return hasha,hashb

a = Account('a', 1000)
b = Account('b', 1000)
mylock = threading.Lock()
#a往b轉賬100
t1 = threading.Thread(target=transfer, args=(a, b, 100))
t1.start()
#b往a轉賬200
t2 = threading.Thread(target=transfer, args=(b, a, 200))
t2.start()
t1.join()
t2.join()
print("a的賬戶余額:",a.balance)
print("b的賬戶余額:",b.balance)
View Code

python中使用上下文管理器來解決動態的鎖順序死鎖問題,當然還是固定鎖的順序操作Python中死鎖的形成示例及死鎖情況的防止

解決方案是為程序中的每一個鎖分配一個唯一的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))
 
  # 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() 函數來申請鎖, 示例如下:

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

如果你執行這段代碼,你會發現它即使在不同的函數中以不同的順序獲取鎖也沒有發生死鎖。 其關鍵在於,在第一段代碼中,我們對這些鎖進行了排序。通過排序,使得不管用戶以什么樣的順序來請求鎖,這些鎖都會按照固定的順序被獲取。

 

開放調用(針對對象之間協作造成的死鎖):

解決協作對象之間發生的死鎖:需要使用開放調用,即避免在持有鎖的情況下調用外部的方法,就是盡量將鎖的范圍縮小,將同步代碼塊僅用於保護那些設計共享狀態的操作

使用定時鎖-:

加上一個超時時間,若一個線程沒有在給定的時限內成功獲得所有需要的鎖,則會進行回退並釋放所有已經獲得的鎖,然后等待一段隨機的時間再重試。

但是如果有非常多的線程同一時間去競爭同一批資源,就算有超時和回退機制,還是可能會導致這些線程重復地嘗試但卻始終得不到鎖。

 

6.死鎖檢測

死鎖檢測:死鎖檢測即每當一個線程獲得了鎖,會在線程和鎖相關的數據結構中( map 、 graph 等)將其記下。除此之外,每當有線程請求鎖,也需要記錄在這個數據結構中。死鎖檢測是一個更好的死鎖預防機制,它主要是針對那些不可能實現按序加鎖並且鎖超時也不可行的場景。
其中,死鎖檢測最出名的算法是由艾茲格·迪傑斯特拉在 1965 年設計的銀行家算法,通過記錄系統中的資源向量、最大需求矩陣、分配矩陣、需求矩陣,以保證系統只在安全狀態下進行資源分配,由此來避免死鎖。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM