Python多线程死锁原理与RLock解决方案详解

Aria
Aria
发布时间:2026-10-15| 5人浏览过
原创

本文深入解析Python多线程环境下的死锁问题,通过银行家算法类比阐述资源分配原理。重点演示了传统互斥锁导致死锁的代码场景,并详细讲解如何利用RLock递归锁解决同一线程多次请求资源的问题,附带完整代码示例与输出分析。

在Python多线程编程中,解决死锁问题主要有两种思路:一是为锁操作添加超时时间,避免无限等待;二是使用支持重入的锁机制。


fromthreading import Thread, Lock
importtime 
mutex1= Lock()  # 创建一个互斥锁
mutex2= Lock()  # 创建一个互斥锁

def fun1():
    while True:
        mutex1.acquire()
        print("线程1 锁住了mutex1")
        time.sleep(0.1)
 
        result =mutex2.acquire(timeout=1)  # timeout指明acquire等的最长超时时间
        # result = mutex2.acquire(False)  # 非阻塞
        if result:
            # 表示对mutex2成功上锁
            print("线程1 锁住了mutex2")
            print("线程1 hello")
            mutex1.release()
            mutex2.release()
            break
        else:
            # 表示对mutex2上锁失败
           mutex1.release()  # 将mutex1释放,保证别人能够执行
            time.sleep(0.1)

def fun2():
    mutex2.acquire()
    print("线程2 锁住了mutex2")
    time.sleep(0.1)
    mutex1.acquire()
    print("线程2 锁住了mutex1")
    print("线程2 hi") 
    mutex1.release()
    mutex2.release()

附录部分将介绍银行家算法,该算法虽非Python标准库内容,但有助于理解操作系统中资源分配与死锁避免的核心逻辑,建议读者理解其思想即可。

背景知识

银行家算法源于一个经典的资源分配场景:银行家需将资金安全地借给多个客户,确保每位客户都能完成业务,同时银行家能收回全部资金而不破产。在操作系统中,这对应于进程资源分配问题:银行家相当于操作系统,客户相当于运行中的进程,资金则对应系统资源。

问题描述

假设银行家持有一定数量的资金,有多个客户申请贷款。每个客户需在开始时声明所需贷款总额。若客户申请的总额不超过银行家的资金总数,银行家可接受申请。贷款以单位形式(如1万元)逐步发放,客户在借满前可能需等待,但银行家必须保证这种等待是有限的且最终可完成。

例如,有三个客户C1、C2、C3向银行家借款,银行家总资金为10个单位。其中C1需9个单位,C2需3个单位,C3需8个单位,总计20个单位。某一时刻的状态如图所示。

针对图a的状态,根据安全序列要求,首个被满足的客户所需贷款必须小于等于银行家当前剩余资金。图中仅C2满足条件(C2需1个单位,银行家剩余2个单位)。银行家将1个单位借给C2,C2完成工作后归还3个单位,状态转为图b。同理,银行家借出4个单位给C3,C3完成工作后状态转为图c。此时仅剩C1,其需7个单位,而银行家手中已有8个单位,故C1也能顺利借款并完成工作。最终(见图d),银行家收回全部10个单位,确保不亏损。因此,序列{C1, C2, C3}是一个安全序列。若按此序列贷款,银行家处于安全状态。反之,若在图b状态时银行家将4个单位借给C1,则进入不安全状态:此时C1和C3均无法完成工作,且银行家手中无资金,系统陷入僵持,银行家也无法收回投资。

综上所述,银行家算法从当前状态出发,按安全序列逐一检查各客户能否完成工作。假定某客户完成工作并归还全部贷款后,再检查下一个能完成工作的客户。若所有客户均能完成工作,则找到安全序列,银行家处于安全状态。

补充:Python基础-死锁与递归锁

死锁

死锁是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成互相等待的现象。若无外力作用,这些进程或线程将无法继续推进,此时称系统处于死锁状态,这些永远在互相等待的进程称为死锁进程。


from threading import Thread,Lock
import time
mutexA=Lock()
mutexB=Lock()
class MyThread(Thread):
    def run(self):
        self.func1()
        self.func2()
    def func1(self):
        mutexA.acquire()
        print('\033[41m%s 拿到A锁\033[0m' %self.name)
        mutexB.acquire()
        print('\033[42m%s 拿到B锁\033[0m' %self.name)
        mutexB.release()
        mutexA.release()
    def func2(self):
        mutexB.acquire()
        print('\033[43m%s 拿到B锁\033[0m' %self.name)
        time.sleep(2)
        mutexA.acquire()
        print('\033[44m%s 拿到A锁\033[0m' %self.name)
        mutexA.release()
        mutexB.release()
if __name__ == '__main__':
    for i in range(5):
        t=MyThread()
        t.start()

输出如下:

Thread-1 拿到A锁

Thread-1 拿到B锁

Thread-1 拿到B锁

Thread-2 拿到A锁

分析上述代码如何产生死锁:

启动5个线程执行run方法。假设thread1首先抢到A锁,此时未释放A锁,接着执行代码mutexB.acquire()并成功抢到B锁。在抢B锁时,因A锁未释放,其他线程只能等待。thread1执行完func1后继续执行func2。与此同时,在func2中执行mutexB.acquire()并抢到B锁,随后进入睡眠状态。当thread1执行完func1释放A、B锁时,其他剩余线程开始抢A锁执行func1。若thread2抢到A锁,接下来需抢B锁。在此期间,thread1已执行func2并抢到B锁,随后sleep(2)持有B锁未释放。由于无其他线程争抢,thread1只能睡眠。此时thread1持有B锁,而thread2需抢B锁,从而形成死锁。

递归锁

我们分析了死锁,那么Python中是如何解决这样的递归锁呢?

在Python中,为了支持同一线程多次请求同一资源,Python提供了可重入锁RLock。

RLock内部维护着一个Lock和一个counter变量,counter记录acquire的次数,从而使资源可被多次require。直到一个线程所有的acquire都被release,其他线程才能获得资源。上述例子若使用RLock代替Lock,则不会发生死锁:


from threading import Thread,Lock,RLock
import time
mutexA=mutexB=RLock()
class MyThread(Thread):
    def run(self):
        self.f1()
        self.f2()
    def f1(self):
        mutexA.acquire()
        print('%s 拿到A锁' %self.name)
        mutexB.acquire()
        print('%s 拿到B锁' %self.name)
        mutexB.release()
        mutexA.release()
    def f2(self):
        mutexB.acquire()
        print('%s 拿到B锁' % self.name)
        time.sleep(0.1)
        mutexA.acquire()
        print('%s 拿到A锁' % self.name)
        mutexA.release()
        mutexB.release()
if __name__ == '__main__':
    for i in range(5):
        t=MyThread()
        t.start()

输出代码如下:

E:\python\python_sdk\python.exe "E:/python/py_pro/3 死锁现象与递归锁.py"

Thread-1 拿到A锁

Thread-1 拿到B锁

Thread-1 拿到B锁

Thread-1 拿到A锁

Thread-2 拿到A锁

Thread-2 拿到B锁

Thread-2 拿到B锁

Thread-2 拿到A锁

Thread-4 拿到A锁

Thread-4 拿到B锁

Thread-4 拿到B锁

Thread-4 拿到A锁

Thread-3 拿到A锁

Thread-3 拿到B锁

Thread-3 拿到B锁

Thread-3 拿到A锁

Thread-5 拿到A锁

Thread-5 拿到B锁

Thread-5 拿到B锁

Thread-5 拿到A锁

Process finished with exit code 0

或者如下的效果:

来解释下递归锁的代码:

由于锁A和B是同一个递归锁,thread1拿到A、B锁后,counter记录了acquire的次数为2次。在func1执行完毕后,释放递归锁。thread1释放完递归锁并执行完func1代码后,接下来有两种可能:1、thread1再次抢到递归锁,执行func2代码;2、其他线程抢到递归锁,去执行func1的任务代码。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持本站。如有错误或未考虑完全的地方,望不吝赐教。

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。