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

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

死锁

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

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


img

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

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

上下文管理器

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

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

我们来看一个样例:

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当中也提供了上下文管理的注解,通过使用注解我们可以很方便地实现上下文管理。我们同样也来看一个例子:

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》,非常经典:

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的使用:

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()

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

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函数我们可以非常方便地解决他们死锁吃不了饭的问题。

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. PHP array_intersect_ukey() 函数

    实例 比较两个数组的键名(使用用户自定义函数比较键名),并返回交集: <?phpfunction myfunction($a,$b){if ($a===$b){return 0;}return ...

  2. PHP image_type_to_extension - 获取图片后缀

    image_type_to_extension — 根据指定的图像类型返回对应的后缀名.高佣联盟 www.cgewang.com 语法 string image_type_to_extension ( ...

  3. CF613D Kingdom and its Cities 虚树 树形dp 贪心

    LINK:Kingdom and its Cities 发现是一个树上关键点问题 所以考虑虚树刚好也有标志\(\sum k\leq 100000\)即关键点总数的限制. 首先当k==1时 答案显然为0 ...

  4. Linux下运行windows 系统下编辑的Python脚本显示“: 没有那个文件或目录”的过程及解决方案

    今天在 linux 系统下执行一windows下编辑的python脚本,提示(:没有那个文件或目录)英文提示:(:No such file of directory)如下: 查看文件的权限发现并没有问 ...

  5. 关于SqlServer那些事1(回归基础)

    即将实习,回归基础总结,希望可以再好好打磨一下基础的一些东西 关于如何在重新修改表结构时该变其权限设置 步骤: 点击工具 进入选项 设计器 取消勾选阻止保存要求重新创建表的更改 关于创建创建数据库以及 ...

  6. SpringMvc异常处理和SpringMvc拦截器

    1. 异常处理思路 Controller调用service,service调用dao,异常都是向上抛出的,最终有DispatcherServlet找异常处理器进 行异常的处理. SpringMVC的异 ...

  7. CI4框架应用一 - 环境搭建

    CI框架 (codeigniter)算是一个古老的框架了,由于在工作中一直在使用这个框架,还是比较有感情的.我对CI的感觉就是,简单易用,学习曲线平滑,对于新手友好. 目前CI框架已经更新到CI4了, ...

  8. 基于Asp.net Core 3.1实现的Redis及MemoryCache缓存助手CacheHelper

    这几天在面试,这个关于Redis缓存的博客一直没空写,今天总算有点时间了. 从很久很久之前,我就一直想学Redis了,反正看到各大招聘网上都要求Redis,不学就太落后了. 一开始我是按微软官网文档那 ...

  9. Fixing the train-test resolution discrepancy

  10. 2020重新出发,JAVA语言,什么是JAVA?

    @ 目录 什么是 java? JAVA三大体系 Java SE Java EE JavaME java的主要特性和优势 1. 面向对象 2. 平台无关性 3. 可移植性 4. 简单性 5. 解释执行 ...