本文始发于个人公众号:TechFlow,原创不易,求个关注

今天是Python专题的第25篇文章,我们一起来聊聊多线程开发当中死锁的问题。

死锁

死锁的原理非常简单,用一句话就可以描述完。就是当多线程访问多个锁的时候,不同的锁被不同的线程持有,它们都在等待其他线程释放出锁来,于是便陷入了永久等待。比如A线程持有1号锁,等待2号锁,B线程持有2号锁等待1号锁,那么它们永远也等不到执行的那天,这种情况就叫做死锁。

关于死锁有一个著名的问题叫做哲学家就餐问题,有5个哲学家围坐在一起,他们每个人需要拿到两个叉子才可以吃饭。如果他们同时拿起自己左手边的叉子,那么就会永远等待右手边的叉子释放出来。这样就陷入了永久等待,于是这些哲学家都会饿死。


img

这是一个很形象的模型,因为在计算机并发场景当中,一些资源的数量往往是有限的。很有可能出现多个线程抢占的情况,如果处理不好就会发生大家都获取了一个资源,然后在等待另外的资源的情况。

对于死锁的问题有多种解决方法,这里我们介绍比较简单的一种,就是对这些锁进行编号。我们规定当一个线程需要同时持有多个锁的时候,必须要按照序号升序的顺序对这些锁进行访问。通过上下文管理器我们可以很容易实现这一点。

上下文管理器

首先我们来简单介绍一下上下文管理器,上下文管理器我们其实经常使用,比如我们经常使用的with语句就是一个上下文管理器的经典使用。当我们通过with语句打开文件的时候,它会自动替我们处理好文件读取之后的关闭以及抛出异常的处理,可以节约我们大量的代码。

同样我们也可以自己定义一个上下文处理器,其实很简单,我们只需要实现__enter__和__exit__这两个函数即可。__enter__函数用来实现进入资源之前的操作和处理,那么显然__exit__函数对应的就是使用资源结束之后或者是出现异常的处理逻辑。有了这两个函数之后,我们就有了自己的上下文处理类了。

我们来看一个样例:

  1. class Sample:
        def __enter__(self):
            print('enter resources')
            return self
        
        def __exit__(self, exc_type, exc_val, exc_tb):
            print('exit')
            # print(exc_type)
            # print(exc_val)
            # print(exc_tb)

        def doSomething(self):
            a = 1/1
            return a

    def getSample():
        return Sample()

    if __name__ == '__main__':
        with getSample() as sample:
            print('do something')
            sample.doSomething()

当我们运行这段代码的时候,屏幕上打印的结果和我们的预期是一致的。


image-20200803091558632

我们观察一下__exit__函数,会发现它的参数有4个,后面的三个参数对应的是抛出异常的情况。type对应异常的类型,val对应异常时的输出值,trace对应异常抛出时的运行堆栈。这些信息都是我们排查异常的时候经常需要用到的信息,通过这三个字段,我们可以根据我们的需要对可能出现的异常进行自定义的处理。

实现上下文管理器并不一定要通过类实现,Python当中也提供了上下文管理的注解,通过使用注解我们可以很方便地实现上下文管理。我们同样也来看一个例子:

  1. import time
    from contextlib import contextmanager

    @contextmanager
    def timethis(label):
        start = time.time()
        try:
            yield
        finally:
            end = time.time()
            print('{}: {}'.format(label, end - start))
            
            
    with timethis('timer'):
        pass

在这个方法当中yield之前的部分相当于__enter__函数,yield之后的部分相当于__exit__。如果出现异常会在try语句当中抛出,那么我们编写except对异常进行处理即可。

避免死锁

了解了上下文管理器之后,我们要做的就是在lock的外面包装一层,使得我们在获取和释放锁的时候可以根据我们的需要,对锁进行排序,按照升序的顺序进行持有。

这段代码源于Python的著名进阶书籍《Python cookbook》,非常经典:

  1. from contextlib import contextmanager

    # 用来存储local的数据
    _local = threading.local()

    @contextmanager
    def acquire(*locks):
     # 对锁按照id进行排序
        locks = sorted(locks, key=lambda x: id(x))

        # 如果已经持有锁当中的序号有比当前更大的,说明策略失败
        acquired = getattr(_local,'acquired',[])
        if acquired and max(id(lock) for lock in acquired) >= id(locks[0]):
            raise RuntimeError('Lock Order Violation')

        # 获取所有锁
        acquired.extend(locks)
        _local.acquired = acquired

        try:
            for lock in locks:
                lock.acquire()
            yield
        finally:
            # 倒叙释放
            for lock in reversed(locks):
                lock.release()
            del acquired[-len(locks):]

这段代码写得非常漂亮,可读性很高,逻辑我们都应该能看懂,但是有一个小问题是这里用到了threading.local这个组件。

它是一个多线程场景当中的共享变量,虽然说是共享的,但是对于每个线程来说读取到的值都是独立的。听起来有些难以理解,其实我们可以将它理解成一个dict,dict的key是每一个线程的id,value是一个存储数据的dict。每个线程在访问local变量的时候,都相当于先通过线程id获取了一个独立的dict,再对这个dict进行的操作。

看起来我们在使用的时候直接使用了_local,这是因为通过线程id先进行查询的步骤在其中封装了。不明就里的话可能会觉得有些难以理解。

我们再来看下这个acquire的使用:

  1. x_lock = threading.Lock()
    y_lock = threading.Lock()

    def thread_1():
        while True:
            with acquire(x_lock, y_lock):
                print('Thread-1')

    def thread_2():
        while True:
            with acquire(y_lock, x_lock):
                print('Thread-2')

    t1 = threading.Thread(target=thread_1)
    t1.start()

    t2 = threading.Thread(target=thread_2)
    t2.start()

运行一下会发现没有出现死锁的情况,但如果我们把代码稍加调整,写成这样,那么就会触发异常了。

  1. def thread_1():
        while True:
            with acquire(x_lock):
                with acquire(y_lock):
                 print('Thread-1')

    def thread_2():
        while True:
            with acquire(y_lock):
                with acquire(x_lock):
                 print('Thread-1')

因为我们把锁写成了层次结构,这样就没办法进行排序保证持有的有序性了,那么就会触发我们代码当中定义的异常。

最后我们再来看下哲学家就餐问题,通过我们自己实现的acquire函数我们可以非常方便地解决他们死锁吃不了饭的问题。

  1. import threading

    def philosopher(left, right):
        while True:
            with acquire(left,right):
                 print(threading.currentThread(), 'eating')

    # 叉子的数量
    NSTICKS = 5
    chopsticks = [threading.Lock() for n in range(NSTICKS)]

    for n in range(NSTICKS):
        t = threading.Thread(target=philosopher,
                             args=(chopsticks[n],chopsticks[(n+1) % NSTICKS]))
        t.start()

总结

关于死锁的问题,对锁进行排序只是其中的一种解决方案,除此之外还有很多解决死锁的模型。比如我们可以让线程在尝试持有新的锁失败的时候主动放弃所有目前已经持有的锁,比如我们可以设置机制检测死锁的发生并对其进行处理等等。发散出去其实有很多种方法,这些方法起作用的原理各不相同,其中涉及大量操作系统的基础概念和知识,感兴趣的同学可以深入研究一下这个部分,一定会对操作系统以及锁的使用有一个深刻的认识。

今天的文章到这里就结束了,如果喜欢本文的话,请来一波素质三连,给我一点支持吧(关注、转发、点赞)。

- END -

扫码关注,获取更多文章

Python | 多线程死锁问题的巧妙解决方法的更多相关文章

  1. $ sudo python -m pip install pylint 出错解决方法

    问题:在unbuntu执行$ sudo python -m pip install pylint出错解决方法支行以下命令sudo pip install pylint==1.9.3这样roboware ...

  2. python用户评论标签匹配的解决方法

    python用户评论标签匹配的解决方法 这篇文章主要为大家详细介绍了python用户评论标签匹配的解决方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下 我们观察用户评论发现:属性词往往和情感词伴 ...

  3. Python实现全局变量的两个解决方法

    Python实现全局变量的两个解决方法 本文针对Python的全局变量实现方法简述如下: 先来看下面一段测试程序:     count = 0 def Fuc(count):   print coun ...

  4. win安装python模块出现依赖问题的解决方法 & No module named 'MySqldb'

    前言 一年多了,还在写这种问题,羞愧. 新公司不让用自己的电脑,配的winPC,项目启不起来,之前也出现过这个问题,是py3缺少某个模块,但是自己没记,这次记一下好了. No module named ...

  5. 【java 多线程】多线程并发同步问题及解决方法

    一.线程并发同步概念 线程同步其核心就在于一个“同”.所谓“同”就是协同.协助.配合,“同步”就是协同步调昨,也就是按照预定的先后顺序进行运行,即“你先,我等, 你做完,我再做”. 线程同步,就是当线 ...

  6. MSSQL死锁产生原因及解决方法

    一.    什么是死锁 死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去.此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等的进 ...

  7. 【Python】Non-ASCII character '\xe6' 错误解决方法

    刚刚在写Python程序的时候遇到了一个问题,无论是在程序中什么地方出现中文字符,都会出现如下错误 SyntaxError: Non-ASCII character '\xe6' 网上查阅了一下这应该 ...

  8. python积累二:中文乱码解决方法

    根据网上提供的解决方法:添加#coding=utf-8或# -*- coding: utf-8 -*- #coding=utf-8 print "还不行?" 执行结果:还是乱码!: ...

  9. MySQL死锁问题分析及解决方法实例详解(转)

      出处:http://www.jb51.net/article/51508.htm MySQL死锁问题是很多程序员在项目开发中常遇到的问题,现就MySQL死锁及解决方法详解如下: 1.MySQL常用 ...

随机推荐

  1. MacOS下Nginx安装

    1. 先安装homebrew 2. 安装Nginx,终端下执行: $ brew install nginx 安装过程中会自己安装依赖: 3. 启动nginx服务 $ nginx 成功后,使用浏览器打开 ...

  2. SELECT from Nobel Tutorial

    02.SELECT from Nobel Tutorial 注意:where语句中对表示条件的需要用单引号, 下面的译文使用的是有道翻译如有不正确,请直接投诉有道 01.Change the quer ...

  3. Python实现微信读书辅助工具

    [TOC] ##项目来源 这个有意思的项目是我从GitHub上找来的,起因是在不久前微信读书突然就设置了非会员书架数目上限,我总想做点什么来表达我的不满,想到可否用爬虫来获取某一本书的内容, 但是我技 ...

  4. 还分不清 Cookie、Session、Token、JWT?一篇文章讲清楚

    还分不清 Cookie.Session.Token.JWT?一篇文章讲清楚 转载来源 公众号:前端加加 作者:秋天不落叶 什么是认证(Authentication) 通俗地讲就是验证当前用户的身份,证 ...

  5. Ubuntu环境下使用Jupyter Notebook查找桌面.csv文档的方法

    这个问题困扰了我很久,最后在一个老师发来的完成结果里找到了答案.(奇怪的是教材里没有.老师也不讲.尤其是百度也没有啊啊啊啊) 好了进入正题.教材里的原话是这样的 这行代码实现的环境应该是在window ...

  6. 前端面试基础题:Ajax原理

    Ajax 的原理简单来说是在⽤户和服务器之间加了—个中间层( AJAX 引擎),通过XmlHttpRequest 对象来向服务器发异步请求,从服务器获得数据,然后⽤ javascrip t 来操作 D ...

  7. Python selenium爬虫实现定时任务过程解析

    现在需要启动一个selenium的爬虫,使用火狐驱动+多线程,大家都明白的,现在电脑管家显示CPU占用率20%,启动selenium后不停的开启浏览器+多线程, 好,没过5分钟,CPU占用率直接拉到9 ...

  8. .NetCore 配合 Gitlab CI&CD 实践 - 单体项目

    前言 上一篇博文 .NetCore 配合 Gitlab CI&CD 实践 - 开篇,主要简单的介绍了一下 GitLab CI 的持续集成以及持续部署,这篇将通过 GitLab CI 发布一个 ...

  9. 曲线生成与求交—Bezier曲线

    Bezier曲线生成 法国工程师Pierre Bezier在雷诺公司使用该方法来设计汽车.一条Bezier曲线可以拟合任何数目的控制点. 公式 设\(n+1\)个控制点\(P_0,P_1--P_n\) ...

  10. [leetcode/lintcode 题解] 前序遍历和中序遍历树构造二叉树

    [题目描述] 根据前序遍历和中序遍历树构造二叉树. 在线评测地址: https://www.jiuzhang.com/solution/construct-binary-tree-from-preor ...