11月2日死锁问题 + 解决问题

发布时间 2023-11-02 14:47:39作者: songjunwan

死锁问题 + 解决问题

什么是死锁问题?字面意思就是光上锁了没有解锁。

现在有三个线程,这三个线程进行这个上锁与解锁的过程,总共有两次比拼,但是只有两把锁分别是锁1和锁2。

这期间它们比拼谁先抢到锁,线程1抢的最快抢到了锁1,但是线程1不满足就抢一把锁它转手就把锁2也抢走了,导致线程2和线程3只能等它用完释放了才能抢这些锁,线程1完成了第一次比拼将第一次比拼的锁1、2都释放了。

线程2这才运行第一次比拼的锁1和锁2,线程3只能等着。、

线程1开始第二次比拼时运行锁2的时候睡着了,导致没有释放锁然后这时候线程2来到第二次比拼无法拿到锁2导致代码无法运行卡在了,这就是死锁问题。

代码如下

import time
from threading import Thread, Lock

mutex1 = Lock()
mutex2 = Lock()


class MyThreada(Thread):
    def run(self):
        self.task1()
        self.task2()

    # 这里就是第一次运行的锁
    def task1(self):
        mutex1.acquire()
        print(f'{self.name} 抢到了 锁1')

        mutex2.acquire()

        print(f'{self.name} 抢到了 锁2')

        mutex2.release()
        print(f'{self.name} 释放了 锁2')

        mutex1.release()
        print(f'{self.name} 释放了 锁1')

    # 这里就是第二次运行的锁
    def task2(self):
        mutex2.acquire()
        print(f'{self.name} 抢到了 锁2')

        time.sleep(2)

        mutex1.acquire()

        print(f'{self.name}抢到了 锁1')

        mutex1.release()
        print(f'{self.name} 释放了 锁1')

        mutex2.release()
        print(f'{self.name} 释放了 锁2')


if __name__ == '__main__':

    for i in range(3):
        t = MyThreada()
        t.start()

运行结果如下

结果解析:

出现死锁问题还是和线程之间争抢CPU执行权有关,线程1在运行锁2时因为睡眠了两秒导致执行权被线程2拿走,导致线程1无法释放锁2这就会导致task2这个函数的开始处被封死,哪怕线程2执行完了锁1也无法进入task2这个函数里面,然后线程3收到影响也就无法出来,最后这个代码被迫卡死。(这就是死锁问题)

这里给个比喻:

你和你的敌人被困在一个密封的屋子,这个屋子有内门和外门,你手上有内门的钥匙,你的敌人有外门的钥匙。现在你们都知道你们需要两把钥匙才能打开门离开房间。然而,现在的问题就是你们都不信任对方都拒绝现将手中的钥匙交给对方,因为你们都害怕对方黑吃黑。

这种情况陷入了僵局,因为你们都陷入等待对方的局面,但又不愿意主动行动,这就是死锁的比喻。

概念:两个线程

线程1拿到了(锁头2)想要往下执行需要(锁头1)

线程2拿到了(锁头1)想要往下执行需要(锁头2)

互相都拿到了彼此想要往下执行的必需条件,彼此都不撒手,造成了死锁问题

解决方案

方案一:递归锁

递归锁:在同一个线程内可以被多次进行上锁。

如何释放呢?它的内部相当于维护了一个计数器也就是说同一个线程上锁了几次就要解锁几次

代码如下

import time
from threading import Thread, Lock, RLock

mutex1 = RLock()#这里要注意一下有个坑
mutex2 = mutex1#不能让mutex2 = RLock()因为这会导致死锁问题无法解决





#递归锁解决方式

class MyThreada(Thread):
    def run(self):
        self.task1()
        self.task2()

    # 这里就是第一次运行的锁
    def task1(self):
        mutex1.acquire()
        print(f'{self.name} 抢到了 锁1')

        mutex2.acquire()

        print(f'{self.name} 抢到了 锁2')

        mutex2.release()
        print(f'{self.name} 释放了 锁2')

        mutex1.release()
        print(f'{self.name} 释放了 锁1')

    # 这里就是第二次运行的锁
    def task2(self):
        mutex2.acquire()
        print(f'{self.name} 抢到了 锁2')

        time.sleep(2)  # 这里让线程睡眠2导致下面的代码无法正常运行

        mutex1.acquire()

        print(f'{self.name}抢到了 锁1')

        mutex1.release()
        print(f'{self.name} 释放了 锁1')

        mutex2.release()
        print(f'{self.name} 释放了 锁2')


if __name__ == '__main__':

    for i in range(3):
        t = MyThreada()
        t.start()

这个代码有一点需要注意一下:

mutex1mutex2 都是 RLock 时,线程在 task2 中会发生死锁,因为线程首先获取 mutex2,然后尝试获取 mutex1,这时候就会发生死锁,因为需要按照相同数量的 release 来释放这两个锁。

mutex2 设置为 mutex1,就能够正常运行,因为 task2 中的锁获取和释放顺序与 task1 中相同,这避免了死锁。

这个问题的比喻版:

mutex1mutex2 都是 RLock 时,这就像两个人在房间里,每个人手里都有一把锁,他们必须按特定的顺序打开锁,否则房间的门将被永久锁住。如果一个人试图打开第一个锁,然后尝试打开第二个锁,而另一个人尝试按相反的顺序打开锁,那么房间的门将永远无法打开。只有当两个人按相同的顺序打开锁时,房间的门才能够打开,这避免了死锁。

然后具体结果如下

这里将代码修改一下让结果更利于理解

#递归锁解决方式
import time
from threading import Thread, Lock, RLock

mutex1 = RLock()#这里要注意一下有个坑
mutex2 = mutex1#不能让mutex2 = RLock()因为这会导致死锁问题无法解决

class MyThreada(Thread):
    def run(self):
        self.task1()
        self.task2()

    # 这里就是第一次运行的锁
    def task1(self):
        mutex1.acquire()
        print(f'{self.name} 抢到了第一次的 锁1')

        mutex2.acquire()

        print(f'{self.name} 抢到了第一次的 锁2')

        mutex2.release()
        print(f'{self.name} 释放了第一次的 锁2')

        mutex1.release()
        print(f'{self.name} 释放了第一次的 锁1')

    # 这里就是第二次运行的锁
    def task2(self):
        mutex2.acquire()
        print(f'{self.name} 抢到了第二次的 锁2')

        time.sleep(2)  # 这里让线程睡眠2导致下面的代码无法正常运行

        mutex1.acquire()

        print(f'{self.name}抢到了第二次的 锁1')

        mutex1.release()
        print(f'{self.name} 释放了第二次的 锁1')

        mutex2.release()
        print(f'{self.name} 释放了第二次的 锁2')


if __name__ == '__main__':

    for i in range(3):
        t = MyThreada()
        t.start()

结果如下

这里的结果就说明了同一把锁哪怕被锁了其它的也可以继续用,这就保证了不会出现死锁的问题

递归锁的工作方式:
1.线程首次获得递归锁时,锁被标记为被占用,并与获得它的线程相关联(允许其它的线程一起使用这个锁)。
2.当同一线程尝试再次获得这个锁时,锁会允许线程再次获得,而不会阻塞。
3.每次线程获得锁后,必须相应地释放锁。线程只有释放了与之关联的所有锁,锁才会被完全释放,其他线程才能获取它。

当你创建两个递归锁mutex1mutex2时,就好像你有了两把钥匙,每把钥匙可以解锁一个门。通常来说,这两把钥匙是相互独立的,不会相互影响。但是,如果你将mutex2设置为mutex1,就好像你复制了一把钥匙,两把钥匙现在可以同时打开相同的门。这意味着你可以在不发生冲突的情况下打开这扇门,因为你实际上只有一把钥匙,不需要争夺它。这样,你可以避免死锁,因为你的线程现在可以正确地管理这扇门的开启和关闭。