死锁
死锁是指两个或两个以上的线程在执行过程中,由于各自持有一部分共有资源或者彼此通信而造成的一种阻塞的现象。若没有外力作用,线程们将无法继续执行,一直处于阻塞状态。在使用Lock对象给资源加锁时,若操作不当很容易造成死锁,常见的不当行为主要包括上锁与解锁的次数不匹配和两个线程各自持有一部分共享资源。
1. 上锁与解锁次数不匹配
例如,创建一个Lock对象mutex_lock,连续上锁两次,只解锁一次,代码如下:
from threading import Thread, Lock
def do_work():
mutex_lock.acquire() # 上锁,将互斥锁置为锁定状态
mutex_lock.acquire() # 再次上锁
mutex_lock.release() # 解锁,将互斥锁置为非锁定状态
if __name__ == '__main__':
mutex_lock = Lock()
thread = Thread(target=do_work)
thread.start()
以上程序执行后始终无法结束,只能主动停止运行。
2. 两个线程互相使用对方的互斥锁
自定义两个线程,它们分别将互斥锁lock_a和lock_b进行多次上锁和解锁,代码如下:
1 from threading import Thread, Lock
2 import time
3 class ThreadOne(Thread): # 自定义线程ThreadOne
4 def run(self):
5 if lock_a.acquire(): # 若lock_a可上锁
6 print(self.name + ':lock_a上锁')
7 time.sleep(1)
8 if lock_b.acquire(): # 若lock_b可上锁
9 print(self.name + ':lock_b解锁')
10 lock_b.release() # lock_b解锁
11 lock_a.release() # lock_a解锁
12 class ThreadTwo(Thread): # 自定义线程ThreadTwo
13 def run(self):
14 if lock_b.acquire(): # 若lock_b可上锁
15 print(self.name + ':lock_b上锁')
16 time.sleep(1)
17 if lock_a.acquire(): # 若lock_a可上锁
18 print(self.name + ':lock_a解锁')
19 lock_a.release() # lock_a解锁
20 lock_b.release() # lock_b解锁
21 if __name__ == '__main__':
22 lock_a = Lock()
23 lock_b = Lock()
24 thread_one = ThreadOne(name='线程1')
25 thread_two = ThreadTwo(name='线程2')
26 thread_one.start()
27 thread_two.start()
上述代码中,第3~11行定义了线程类ThreadOne,该类重写了run()方法,重新定义了线程的行为。其中,第5~11行使用if嵌套语句进行判断,若互斥锁lock_a处于非锁定状态,首先打印“线程:上锁”,然后继续判断互斥锁lock_b是否处于锁定状态,最后调用release()方法解锁;若lock_b处于锁定状态,打印“线程**:解锁”,调用release()方法解锁。
第12~20行定义了另一个线程类ThreadTwo,该类中同样重写了run()方法。其中,第14~20行使用if嵌套语句进行判断,若lock_b处于非锁定状态,首先打印“线程:上锁”,然后继续判断互斥锁lock_a是否处于锁定状态,最后调用release()方法解锁;若lock_a处于锁定状态,打印“线程**:解锁”,调用release()方法解锁。
第21~27行是main语句,首先创建了两个互斥锁lock_a和lock_b,然后创建了两个线程对象thread_one和thread_two,分别命名为线程1和线程2,最后调用start()方法分别执行线程。
执行程序,程序在打印了如下语句后没有任何反应,也没有终止:
线程1:lock_a上锁
线程2:lock_b上锁
由以上结果可以推测,线程1优先执行将lock_a进行上锁,线程2紧接着开始执行,它将lock_b进行上锁。当线程1需要将lock_b上锁时,线程2已经将lock_b上锁,它只能等待线程2释放lock_b;同时,线程2对lock_a上锁时发现线程1已经将lock_a上锁,因此它只能等待线程1释放lock_a,程序因陷入死锁而一直未能终止。
若产生像第一种死锁的情况,可以直接增加解锁的代码,使得上锁和解锁的次数相匹配。若产生像第二种死锁的情况,可以设置锁定的时长,即调用acquire()方法时为timeout参数传入值。例如,在ThreadOne类中将lock_b上锁时设置超时时长为两秒,代码如下:
......
if lock_a.acquire(): # lock_a上锁
print(self.name + ':lock_a上锁')
time.sleep(1)
if lock_b.acquire(timeout=2): # lock_b上锁
print(self.name + ':lock_b解锁')
lock_b.release() # lock_b解锁
......
再次执行程序,程序在两秒钟以后终止执行,并打印了以下语句:
线程1:lock_a上锁
线程2:lock_b上锁
线程2:lock_a解锁
由以上语句推测可知,线程1和线程2分别对lock_a和lock_b上锁后处于阻塞状态,它们都在等待对方先解锁。阻塞了两秒钟之后,线程1因超过等待的时长而继续向下执行,将lock_a解锁;线程2发现lock_a当前处于非锁定状态,它对lock_a上锁后再解锁,打印“线程2:lock_a解锁”。