互斥锁
Python中的互斥锁(Mutex,Mutual Exclusion)是一种同步原语,用于防止多个线程同时访问共享资源。在Python中,互斥锁通常是通过threading
模块中的Lock
类来实现的。
前面文章将的就是互斥锁:python中的锁
死锁
互斥锁的使用过程中, 如果使用不当,不管是进程或线程的Lock都会出现死锁。死锁(Deadlock)是多线程编程中常见的问题,是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象。
比如当线程A持有资源1并等待获取资源2,而线程B持有资源2并等待获取资源1时,如果没有任何机制来解决这种循环等待,那么这两个线程就会陷入死锁状态,导致程序无法继续执行。此时称程序处于死锁状态或产生了死锁。
Python标准库中的threading
模块并没有直接提供避免死锁的机制,因此开发者需要自己设计策略来预防或解决死锁问题。
必要条件
1. 互斥条件(Mutual Exclusion)
资源不能被多个进程同时使用,即每个资源要么是空闲的,要么是被一个进程独占使用。例如,打印机、数据库连接等都是互斥资源。
2. 占有且等待(Hold and Wait)
一个进程已经持有了至少一个资源,并且在等待获取额外的资源,而这些额外的资源被其他进程持有。例如,一个线程已经持有了 lock1
,但在等待 lock2
,而 lock2
被另一个线程持有。
3. 不可剥夺(No Preemption)
资源不能被强制从一个进程中剥夺,只能由持有该资源的进程显式地释放。例如,一个线程不能强制另一个线程释放它正在使用的锁。
4. 循环等待(Circular Wait)
存在一个进程链,使得每个进程都在等待链中的下一个进程所持有的资源。例如,线程 A 持有 lock1
并等待 lock2
,而线程 B 持有 lock2
并等待 lock1
。
死锁的情况
多个锁
当多个线程尝试以不同的顺序获取多个锁时,可能会发生死锁。
import threading
lock1 = threading.Lock()
lock2 = threading.Lock()
def thread1():
with lock1:
print("Thread 1 acquired lock 1")
# 模拟一些工作
with lock2:
print("Thread 1 acquired lock 2")
def thread2():
with lock2:
print("Thread 2 acquired lock 2")
# 模拟一些工作
with lock1:
print("Thread 2 acquired lock 1")
t1 = threading.Thread(target=thread1)
t2 = threading.Thread(target=thread2)
t1.start()
t2.start()
t1.join()
t2.join()
print("程序结束")
- 线程 1 先获取
lock1
,然后尝试获取lock2
。 - 线程 2 先获取
lock2
,然后尝试获取lock1
。
如果线程 1 在持有 lock1
的同时,线程 2 持有 lock2
,那么两个线程都会等待对方释放锁,从而导致死锁。
嵌套锁
import threading
from threading import Lock
lock = Lock()
with lock:
with lock:
print(threading.current_thread())
print("程序完成")
- 第一次获取锁:
with lock:
成功获取了lock
。 - 第二次获取同一个锁:再次尝试
with lock:
,但此时lock
已经被当前线程持有,因此会阻塞在这里,导致死锁。
解决
方法一:破坏互斥条件
尽量减少对互斥资源的需求,例如通过设计无锁的数据结构或使用读写锁来允许并发访问。
方法二:破坏占有且等待条件
确保在请求新的资源之前释放当前持有的所有资源。可以通过一次性申请所有需要的资源来实现这一点。
方法三:破坏不可剥夺条件
允许抢占,即当某个进程请求某个已被分配给其他进程但尚未使用完毕的资源时,可以强制将该资源从当前持有者那里抢过来分配给请求者。
方法四:破坏循环等待条件
对所有可能申请到的资源进行排序,并要求所有进程按顺序申请。这种方法可以通过对所有锁进行排序并按顺序获取来实现。
多个锁解决
确保所有线程以相同的顺序获取多个锁。例如,可以确保所有线程总是先获取 lock1
,然后再获取 lock2
。
import threading
lock1 = threading.Lock()
lock2 = threading.Lock()
def thread1():
with lock1:
print("Thread 1 acquired lock 1")
# 模拟一些工作
with lock2:
print("Thread 1 acquired lock 2")
def thread2():
with lock1: # 改变这里,使得 thread2 与 thread1 一致地先获取 lock1
print("Thread 2 acquired lock 1")
# 模拟一些工作
with lock2:
print("Thread 2 acquired lock 2")
t1 = threading.Thread(target=thread1)
t2 = threading.Thread(target=thread2)
t1.start()
t2.start()
t1.join()
t2.join()
print("程序结束")
嵌套锁解决
import threading
from threading import RLock
lock = RLock()
with lock:
with lock:
print(threading.current_thread())
print("程序完成")
使用递归锁:RLock,它允许同一个线程多次获取同一个锁,而不会导致死锁。
递归锁
递归锁(Reentrant Lock,简称 RLock)是一种特殊类型的锁,它允许同一个线程多次获取同一个锁,而不会导致死锁。递归锁内部维护了一个计数器,每次成功获取后计数器加一,释放时计数器减一,当计数器为零时才真正释放该资源。
递归锁的特性
- 可重入性:同一个线程可以多次获取同一个递归锁,而不会被阻塞。
- 计数器:递归锁内部维护了一个计数器,每次获取锁时计数器加一,释放时计数器减一。当计数器为零时,锁才真正被释放。
- 避免死锁:在需要多次获取同一个锁的情况下,可以避免死锁问题。
使用场景
递归锁通常用于需要在同一个线程中多次进入临界区的情况,例如递归函数调用或嵌套的 with
语句。
import threading
from threading import RLock, Lock
lock = RLock()
# lock = Lock()
def recursive_function(n):
with lock:
print(f"当前函数第 {n} 层")
if n > 0:
recursive_function(n - 1)
print(f"当前函数第 {n} 层")
# 创建并启动线程
t = threading.Thread(target=recursive_function, args=(3,))
t.start()
t.join()
print("程序完成")
在这个示例中,recursive_function
函数在每个级别都尝试获取同一个递归锁 lock
。
由于使用了 RLock
,所以即使是同一个线程多次获取该锁也不会导致死锁。
但是如果使用的是普通互斥锁 Lock
,当 recursive_function
尝试第二次获取 lock
时会阻塞自己,从而导致死锁。