死锁必然发生,因代码满足互斥、持有并等待、不可抢占和循环等待四条件:线程1持lock_a等lock_b,线程2持lock_b等lock_a,形成循环依赖,导致双方永久阻塞。

死锁,在多线程编程里,它就像一个狡猾的陷阱,一旦触发,程序就会陷入无尽的等待。它不是一个“可能”发生的问题,而是在特定条件下“必然”会发生的一种僵局。本质上,就是两个或多个线程各自持有一把锁,同时又都在等待对方持有的另一把锁,形成一个循环依赖,谁也无法继续执行。
import threading
import time
# 定义两把锁
lock_a = threading.Lock()
lock_b = threading.Lock()
def thread_function_1():
print("线程1: 尝试获取 lock_a...")
lock_a.acquire()
print("线程1: 已获取 lock_a,等待 0.1 秒...")
time.sleep(0.1) # 引入短暂延迟,增加死锁发生的几率
print("线程1: 尝试获取 lock_b...")
lock_b.acquire()
print("线程1: 已获取 lock_b")
# 执行一些操作
print("线程1: 正在执行任务...")
lock_b.release()
print("线程1: 已释放 lock_b")
lock_a.release()
print("线程1: 已释放 lock_a")
print("线程1: 任务完成。")
def thread_function_2():
print("线程2: 尝试获取 lock_b...")
lock_b.acquire()
print("线程2: 已获取 lock_b,等待 0.1 秒...")
time.sleep(0.1) # 引入短暂延迟,增加死锁发生的几率
print("线程2: 尝试获取 lock_a...")
lock_a.acquire()
print("线程2: 已获取 lock_a")
# 执行一些操作
print("线程2: 正在执行任务...")
lock_a.release()
print("线程2: 已释放 lock_a")
lock_b.release()
print("线程2: 已释放 lock_b")
print("线程2: 任务完成。")
if __name__ == "__main__":
print("主线程: 启动线程1和线程2...")
thread1 = threading.Thread(target=thread_function_1)
thread2 = threading.Thread(target=thread_function_2)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print("主线程: 所有线程已尝试完成。")这个示例程序之所以必然导致死锁,是因为它完美地满足了死锁发生的四个经典条件:互斥、持有并等待、不可抢占和循环等待。让我们一步步剖析:
首先是互斥(Mutual Exclusion):
threading.Lock
lock_a
lock_b
接着是持有并等待(Hold and Wait):当
thread_function_1
lock_a
lock_b
thread_function_2
lock_b
lock_a
然后是不可抢占(No Preemption):我们使用的
threading.Lock
最后,也是最关键的,是循环等待(Circular Wait):
thread_function_1
lock_b
lock_b
thread_function_2
thread_function_2
lock_a
lock_a
thread_function_1
time.sleep(0.1)
避免死锁,说起来容易做起来难,但核心原则是打破死锁的四个条件之一。在实际开发中,我们通常通过以下几种模式来规避:
一个非常有效且常用的策略是统一资源获取顺序。这意味着无论哪个线程,当它需要获取多个锁时,都应按照一个预先规定好的全局顺序来获取。比如,如果你的系统中有
lock_a
lock_b
lock_a
lock_b
lock_a
lock_b
lock_b
lock_a
另一个思路是使用带超时机制的锁获取。比如
lock.acquire(timeout=some_value)
更细粒度的锁控制有时也能帮助。不是所有操作都需要持有大范围的锁。通过识别代码中真正需要互斥保护的临界区,只在必要时才加锁,并尽快释放,可以减少锁的持有时间,从而降低死锁发生的概率。但要注意,过度细粒度的锁也可能导致性能下降和代码复杂度增加。
此外,避免在持有锁的情况下调用外部或未知代码。如果在一个持有锁的块中调用了可能需要其他锁的外部函数,或者该函数本身可能阻塞,那么死锁的风险就会大大增加。这要求我们对代码的依赖关系有清晰的认识。
最后,对于某些复杂的场景,可以考虑使用更高级的同步原语,比如
threading.RLock
调试死锁是件令人头疼的事,因为它往往难以复现,且一旦发生,程序通常就“卡住”了,不报错也不退出。但也不是完全束手无策,以下是一些行之有效的方法:
首先,详细的日志记录是排查死锁的基石。在每次锁的获取 (
acquire
release
DEBUG
其次,使用调试器。现代IDE(如PyCharm、VS Code)提供的多线程调试功能非常强大。你可以设置断点,暂停所有线程的执行,然后检查每个线程的调用栈和当前状态。在Python中,如果一个线程被阻塞在
lock.acquire()
对于已经运行的、卡死的Python程序,生成线程Dumps是一个非常实用的技巧。在Linux/macOS上,你可以向Python进程发送
SIGQUIT
kill -QUIT <pid>
Ctrl+Break
pyrasite
WinDbg
acquire
faulthandler
最后,代码审查和静态分析也是预防死锁的重要手段。在代码提交前,进行严格的代码审查,特别是对涉及多线程和锁操作的代码块。遵循前面提到的“统一资源获取顺序”等模式,可以大大降低死锁的风险。虽然静态分析工具在检测死锁方面能力有限,但它们可以帮助发现一些潜在的并发问题。有时候,最有效的“调试”方式,就是从一开始就写出不会产生死锁的代码。
以上就是请写一个必然会产生死锁的示例程序的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号