【python内功修炼007】:死锁现象与递归锁

一、前言

  • 线程互斥锁分为递归锁和非递归锁。
  • 互斥锁默认是非递归锁
  • 如果一个线程多次获取非递归锁,就会出现死锁现象
  • 如果一个线程可以多次获取同一个递归锁,不会产生死锁。

二、死锁现象

2.1什么是死锁

	指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

​ 此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

2.2 死锁实例

from threading import Thread,RLock,Lock
import time


#  死锁现象
# ==================================================================
mutexA = Lock()  # 锁A
mutexB = Lock()  # 锁B

class Task(Thread):

    def run(self):
        self.F1()
        self.F2()

    def F1(self):
        mutexA.acquire()  # 加A锁
        print('%s 拿到了A锁' % self.name)

        mutexB.acquire()  # 加B锁
        print('%s 拿到了B锁' % self.name)
        mutexB.release()  # 释放B锁

        mutexA.release()  # 释放A锁

    def F2(self):
        mutexB.acquire()  # 加B锁
        print('%s 拿到了B锁' % self.name)
        time.sleep(0.1)  # IO

        mutexA.acquire()    # 加A锁
        print('%s 拿到了A锁' % self.name)
        mutexA.release()  # 释放A锁
        
        mutexB.release()  # 释放B锁


if __name__ == '__main__':
    obj = Task()
    for i in range(10):
        obj = Task()
        obj.start()

结果:

Thread-2 拿到了A锁
Thread-2 拿到了B锁
Thread-2 拿到了B锁
Thread-3 拿到了A锁

# 程序卡死

2.3死锁实例分析

先运行的(假定 Thread-1)线程在F2函数体IO的时候拿到B锁的同时,(假定 Thread-2)在F1函数体内拿到了A锁

Thread-1想继续运行,必须得拿到A锁然后才能释放B锁,Thread-2想继续运行必须拿到B锁然后才能释放A锁,两个线程互不相让,这个时候就导致了阻塞。

三、递归锁

解决死锁问题就要用到递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。

3.1 RLock和Lock的区别

这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。

二者的区别是:递归锁(RLock)可以连续acquire多次,而互斥锁(Lock)只能acquire一次

3.2 递归锁实例

# 怎么解决死锁?
# 运用递归锁  RLock()
# ==================================================================

from threading import Thread,RLock
import time
mutexA = mutexB = RLock()  # 这相当于1把锁,mutexA\ mutexB  只是指向同一个内存地址

class Task(Thread):

    def run(self):
        self.F1()
        self.F2()

    def F1(self):
        mutexA.acquire()  # 加A锁
        print('%s 拿到了A锁' % self.name)

        mutexB.acquire()  # 加B锁
        print('%s 拿到了B锁' % self.name)
        mutexB.release()  # 释放B锁

        mutexA.release()  # 释放A锁

    def F2(self):
        mutexB.acquire()  # 加B锁
        print('%s 拿到了B锁' % self.name)
        time.sleep(1)  # IO

        mutexA.acquire()    # 加A锁
        print('%s 拿到了A锁' % self.name)
        mutexA.release()  # 释放A锁

        mutexB.release()  # 释放B锁


if __name__ == '__main__':
    obj = Task()
    for i in range(10):
        obj = Task()
        obj.start()

结果

Thread-2 拿到了A锁
Thread-2 拿到了B锁
Thread-2 拿到了B锁
Thread-2 拿到了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锁
Thread-7 拿到了A锁
Thread-7 拿到了B锁
Thread-7 拿到了B锁
Thread-7 拿到了A锁
Thread-9 拿到了A锁
Thread-9 拿到了B锁
Thread-9 拿到了B锁
Thread-9 拿到了A锁
Thread-11 拿到了A锁
Thread-11 拿到了B锁
Thread-11 拿到了B锁
Thread-11 拿到了A锁
Thread-6 拿到了A锁
Thread-6 拿到了B锁
Thread-6 拿到了B锁
Thread-6 拿到了A锁
Thread-10 拿到了A锁
Thread-10 拿到了B锁
Thread-10 拿到了B锁
Thread-10 拿到了A锁
Thread-8 拿到了A锁
Thread-8 拿到了B锁
Thread-8 拿到了B锁
Thread-8 拿到了A锁
Thread-4 拿到了A锁
Thread-4 拿到了B锁
Thread-4 拿到了B锁
Thread-4 拿到了A锁

3.3 递归锁实例分析

由于锁A,B是同一个递归锁,Thread-2拿到A,B锁,counter记录了acquire的次数2次,然后在F1函数体代码执行完毕,就释放递归锁,在thread-2释放完递归锁,执行完F1代码,接下来会有2种可能,1、Thread-2再次抢到递归锁,执行F2函数体代码 2、其他的线程抢到递归锁,去执行F1函数体代码

四、互斥锁总结

  • 死锁现象从本质上来讲是一种逻辑错
  • 递归锁没有从根本上解决死锁问题,它只是牺牲了时间和空间的
  • 不提倡使用递归锁,用到递归锁的时候,说明程序有逻辑上的错误
  • 在程序逻辑有问题时,产生死锁暴露出问题。有些逻辑特别混乱或涉及三方开发对外提供服务不得已时,再用递归锁
发布了72 篇原创文章 · 获赞 79 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_42444693/article/details/105282109