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)
动态的锁顺序死锁
# 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)
解决动态的锁顺序死锁的方法:比较传入锁对象的哈希值,根据哈希值的大小来确保所有的线程都以相同的顺序获得锁 。
# 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)
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 年设计的银行家算法,通过记录系统中的资源向量、最大需求矩阵、分配矩阵、需求矩阵,以保证系统只在安全状态下进行资源分配,由此来避免死锁。