死锁是Python多进程开发中最容易踩的坑之一,一旦出现会导致进程卡死、程序无响应,甚至需要强制终止才能恢复。
一、先搞懂:多进程死锁到底是什么?
1. 死锁的核心定义
死锁是指两个或多个进程互相持有对方需要的锁(或资源),且都不释放自己持有的锁,导致所有进程都陷入"等待对方释放资源"的无限阻塞状态。
2. 死锁产生的4个必要条件(缺一不可)
只有同时满足以下4个条件,才会触发死锁,打破任意一个条件就能避免死锁:
- 互斥条件:资源(如锁、文件)只能被一个进程占用;
- 请求与保持条件:进程持有一个资源的同时,又请求另一个被占用的资源;
- 不剥夺条件:进程已持有的资源不能被强制剥夺,只能主动释放;
- 循环等待条件:多个进程形成"你等我、我等你"的循环等待链。
3. 多进程死锁典型场景(新手高频踩坑)
python
import multiprocessing
import time
场景:两个进程互相等待对方的锁
def process1(lock1, lock2):
进程1先拿lock1,再尝试拿lock2
lock1.acquire()
print("进程1获取了lock1,等待lock2...")
time.sleep(1) 放大死锁概率
lock2.acquire() 此时lock2已被进程2持有,进程1阻塞
print("进程1获取了lock2,执行任务")
lock1.release()
lock2.release()
def process2(lock1, lock2):
进程2先拿lock2,再尝试拿lock1
lock2.acquire()
print("进程2获取了lock2,等待lock1...")
time.sleep(1)
lock1.acquire() 此时lock1已被进程1持有,进程2阻塞
print("进程2获取了lock1,执行任务")
lock1.release()
lock2.release()
if __name__ == "__main__":
lock1 = multiprocessing.Lock()
lock2 = multiprocessing.Lock()
p1 = multiprocessing.Process(target=process1, args=(lock1, lock2))
p2 = multiprocessing.Process(target=process2, args=(lock1, lock2))
p1.start()
p2.start()
p1.join()
p2.join()
print("程序结束") 永远不会执行到这一行
现象:程序输出"进程1获取了lock1,等待lock2..."和"进程2获取了lock2,等待lock1..."后卡死,无法继续执行。
二、避免死锁的6个核心策略(从易到难)
1. 策略1:使用with语句自动释放锁(最推荐)
with语句会在代码块执行完成后自动释放锁,即使代码抛出异常也能保证锁释放,从根本上避免"忘记release()"导致的死锁。
修复上述死锁案例(仅改锁的使用方式):
python
def process1(lock1, lock2):
with lock1: 自动acquire(),代码块结束自动release()
print("进程1获取了lock1,等待lock2...")
time.sleep(1)
with lock2:
print("进程1获取了lock2,执行任务")
def process2(lock1, lock2):
with lock2:
print("进程2获取了lock2,等待lock1...")
time.sleep(1)
with lock1:
print("进程2获取了lock1,执行任务")
⚠️ 注意:这个修改仅解决"锁未释放"的问题,但上述场景仍会因"循环等待"触发死锁,需结合策略2使用。
2. 策略2:统一锁的获取顺序(打破循环等待)
死锁的核心诱因之一是"进程获取锁的顺序不一致",只要让所有进程按相同的顺序获取锁,就能打破循环等待条件。
最终修复死锁案例:
python
def process1(lock1, lock2):
统一先拿lock1,再拿lock2
with lock1:
print("进程1获取了lock1,等待lock2...")
time.sleep(1)
with lock2:
print("进程1获取了lock2,执行任务")
def process2(lock1, lock2):
同样先拿lock1,再拿lock2(不再先拿lock2)
with lock1:
print("进程2获取了lock1,等待lock2...")
time.sleep(1)
with lock2:
print("进程2获取了lock2,执行任务")
执行结果:进程1先获取lock1,进程2等待lock1;进程1执行完成释放锁后,进程2获取lock1和lock2,无死锁。
3. 策略3:给锁添加超时时间(避免无限等待)
multiprocessing.Lock本身不支持超时,但可使用multiprocessing.RLock(可重入锁)或threading.Lock(多进程中需通过Manager传递)的acquire(timeout)方法,设置超时时间,超时后放弃获取锁,避免无限阻塞。
示例:带超时的锁获取
python
import multiprocessing
import time
def process1(lock1, lock2):
获取lock1,超时时间3秒
if lock1.acquire(timeout=3):
print("进程1获取了lock1,等待lock2...")
time.sleep(1)
获取lock2,超时时间3秒
if lock2.acquire(timeout=3):
print("进程1获取了lock2,执行任务")
lock2.release()
else:
print("进程1获取lock2超时,释放lock1")
lock1.release()
else:
print("进程1获取lock1超时")
def process2(lock1, lock2):
if lock2.acquire(timeout=3):
print("进程2获取了lock2,等待lock1...")
time.sleep(1)
if lock1.acquire(timeout=3):
print("进程2获取了lock1,执行任务")
lock1.release()
else:
print("进程2获取lock1超时,释放lock2")
lock2.release()
else:
print("进程2获取lock2超时")
if __name__ == "__main__":
通过Manager创建支持超时的锁
manager = multiprocessing.Manager()
lock1 = manager.Lock()
lock2 = manager.Lock()
p1 = multiprocessing.Process(target=process1, args=(lock1, lock2))
p2 = multiprocessing.Process(target=process2, args=(lock1, lock2))
p1.start()
p2.start()
p1.join()
p2.join()
print("程序结束")
执行结果:进程1获取lock1,进程2获取lock2;双方等待对方的锁超时后,释放自己持有的锁,程序正常结束,无死锁。
4. 策略4:减少锁的使用范围(最小化锁持有时间)
只在"必须保护共享资源"的代码块加锁,执行完核心逻辑后立即释放锁,缩短锁的持有时间,降低多个进程同时争用锁的概率。
反面案例(锁持有时间过长):
python
def write_file(num, lock):
lock.acquire()
非核心逻辑(耗时),却持有锁
time.sleep(5) 模拟耗时操作
with open("test.txt", "a") as f:
f.write(f"进程{num}写入\n")
lock.release()
正面案例(仅核心逻辑加锁):
python
def write_file(num, lock):
非核心逻辑(耗时),不持有锁
time.sleep(5)
仅写入文件时加锁
with lock:
with open("test.txt", "a") as f:
f.write(f"进程{num}写入\n")
5. 策略5:使用单锁替代多锁(简化资源竞争)
如果多个锁的作用是保护同一类共享资源(如多个文件都属于"数据文件"),可合并为一个全局锁,避免多锁嵌套导致的死锁。
示例:单锁替代多锁
python
import multiprocessing
import time
全局锁(替代多个文件锁)
global_lock = multiprocessing.Lock()
写入文件A
def write_file_a(num):
with global_lock:
with open("file_a.txt", "a") as f:
f.write(f"进程{num}写入文件A\n")
time.sleep(1)
写入文件B
def write_file_b(num):
with global_lock:
with open("file_b.txt", "a") as f:
f.write(f"进程{num}写入文件B\n")
time.sleep(1)
if __name__ == "__main__":
p1 = multiprocessing.Process(target=write_file_a, args=(1,))
p2 = multiprocessing.Process(target=write_file_b, args=(2,))
p1.start()
p2.start()
p1.join()
p2.join()
print("写入完成")
6. 策略6:使用死锁检测工具(进阶)
对于复杂的多进程场景,可通过工具检测潜在的死锁:
-
内置工具 :
multiprocessing.active_children()查看活跃进程,结合日志定位阻塞的进程; -
第三方工具 :
py-spy(进程分析工具),可实时查看进程调用栈,定位死锁位置:bash安装py-spy pip install py-spy 分析进程(替换为你的进程ID) py-spy dump --pid 12345
三、死锁排查:快速定位问题的3个方法
- 查看进程状态 :用
ps -ef | grep python查看进程是否处于"D状态"(不可中断睡眠,大概率死锁); - 添加日志 :在每个
acquire()和release()前后打印日志,定位哪个锁导致阻塞; - 简化场景:将复杂的多进程逻辑拆分为最小可复现的demo,逐步排查死锁诱因。
四、总结:避免死锁的核心原则
- 自动释放 :优先用
with语句管理锁,杜绝"忘记release()"; - 顺序一致:所有进程按相同顺序获取多把锁,打破循环等待;
- 超时兜底:给锁添加超时时间,避免无限阻塞;
- 最小持有:仅在核心逻辑加锁,缩短锁的持有时间;
- 简化锁结构:能用单锁就不用多锁,减少嵌套争用。
遵循这些原则,99%的多进程死锁问题都能被规避。新手建议从"with语句+统一锁顺序"这两个最基础的策略入手,先保证程序无死锁,再根据场景优化性能。