举例
本文对Python死锁文章进行总结,对部分难以理解内容做了解释。
首先先列举一个会发生死锁的情况
死锁的一个原因是互斥锁。假设银行系统中,用户a试图转账100块给用户b,
与此同时用户b试图转账200块给用户a,则可能产生死锁。
2个线程互相等待对方的锁,互相占用着资源不释放。
#coding=utf-8
import time
import threading
class Account:
def __init__(self, _id, balance, lock):
self.id = _id
self.balance = balance
self.lock = lock
def withdraw(self, amount):
self.balance -= amount
def deposit(self, amount):
self.balance += amount
def transfer(_from, to, amount):
if _from.lock.acquire():#锁住自己的账户
_from.withdraw(amount)
time.sleep(1)#让交易时间变长,2个交易线程时间上重叠,有足够时间来产生死锁
print('wait for lock...')
if to.lock.acquire():#锁住对方的账户
to.deposit(amount)
to.lock.release()
_from.lock.release()
print( 'finish...')
a = Account('a',1000, threading.Lock())
b = Account('b',1000, threading.Lock())
threading.Thread(target = transfer, args = (a, b, 100)).start()
threading.Thread(target = transfer, args = (b, a, 200)).start()
在多线程程序中,死锁问题很大一部分是由于线程同时获取多个锁造成的。举个例子:一个线程获取了第一个锁,然后在获取第二个锁的 时候发生阻塞,那么这个线程就可能阻塞其他线程的执行,从而导致整个程序假死。 解决死锁问题的一种方案是为程序中的每一个锁分配一个唯一的id,然后只允许按照升序规则来使用多个锁,这个规则使用上下文管理器 是非常容易实现的
先来看一下什么是上下文管理器@contextmanager
被装饰器装饰的函数分为三部分:
- with语句中的代码块执行前执行函数中yield之前代码
- yield返回的内容复制给as之后的变量
- with代码块执行完毕后执行函数中yield之后的代码
解决方案
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', [])
#如果出现新锁id大于旧锁id 报错
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 #无返回值 yield为空
finally:
# Release locks in reverse order of acquisition
for lock in reversed(locks):
lock.release()
del _local.acquired[-len(locks):]
当然采用升序获取锁也存在一定的问题,看下列代码
import threading
x_lock = threading.Lock()
y_lock = threading.Lock()
def thread_1():
while True:
with acquire(x_lock):
print("acquire x_lock")
time.sleep(1)
with acquire(y_lock):
print('Thread-1')
time.sleep(1)
def thread_2():
while True:
with acquire(y_lock):
print('acquire y_lock')
time.sleep(1)
with acquire(x_lock):
print('Thread-2')
t1 = threading.Thread(target=thread_1)
t1.daemon = False
t1.start()
t2 = threading.Thread(target=thread_2)
t2.daemon = False
t2.start()
daemon:
(1)如果某个子线程的daemon属性为False,主线程结束时会检测该子线程是否结束,
如果该子线程还在运行,则主线程会等待它完成后再退出;
(2)如果某个子线程的daemon属性为True,主线程运行结束时不对这个子线程进行检
查而直接退出,同时所有daemon值为True的子线程将随主线程一起结束,而不论
是否运行完成。
代码运行结果为出现异常
发生崩溃的原因在于,每个线程都记录着自己已经获取到的锁。 acquire() 函数会检查之前已经获取的锁列表, 由于锁是按照升序排列获取的,所以函数会认为之前已获取的锁的id必定小于新申请到的锁,这时就会触发异常。
出现上述结果的原因是异常先出现在Thread-2,导致该线程中断。之后不会发生死锁现象了,因为只有一个线程Thread-1在运行,并且通过上下文管理器,每次内部acquire调用完毕后会自动释放锁,所以不会出现死锁现象了。
总结:
避免死锁是一种解决死锁问题的方式,在进程获取锁的时候会严格按照对象id升序排列获取,经过数学证明,这样保证程序不会进入 死锁状态。避免死锁的主要思想是,单纯地按照对象id递增的顺序加锁不会产生循环依赖,而循环依赖是 死锁的一个必要条件,从而避免程序进入死锁状态。