python 多线程死锁问题的解决方案


Posted in Python onAugust 25, 2020

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

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

python 多线程死锁问题的解决方案

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

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

上下文管理器

首先我们来简单介绍一下上下文管理器,上下文管理器我们其实经常使用,比如我们经常使用的 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()

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

python 多线程死锁问题的解决方案

我们观察一下__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()

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

以上就是python 多线程死锁问题的解决方案的详细内容,更多关于python 多线程死锁的资料请关注三水点靠木其它相关文章!

Python 相关文章推荐
Python 获取新浪微博的最新公共微博实例分享
Jul 03 Python
Python中unittest用法实例
Sep 25 Python
利用Python如何生成随机密码
Apr 20 Python
python MySQLdb使用教程详解
Mar 20 Python
Flask-Mail用法实例分析
Jul 21 Python
Flask框架Flask-Principal基本用法实例分析
Jul 23 Python
详解Python用三种方式统计词频的方法
Jul 29 Python
python检测服务器端口代码实例
Aug 31 Python
python配置文件写入过程详解
Oct 19 Python
Python上下文管理器类和上下文管理器装饰器contextmanager用法实例分析
Nov 07 Python
使用Python实现牛顿法求极值
Feb 10 Python
Django用户认证系统如何实现自定义
Nov 12 Python
详解Pycharm与anaconda安装配置指南
Aug 25 #Python
Python如何爬取51cto数据并存入MySQL
Aug 25 #Python
基于Python爬取51cto博客页面信息过程解析
Aug 25 #Python
Python使用requests模块爬取百度翻译
Aug 25 #Python
Python爬虫使用bs4方法实现数据解析
Aug 25 #Python
Python+Opencv身份证号码区域提取及识别实现
Aug 25 #Python
Python Selenium实现无可视化界面过程解析
Aug 25 #Python
You might like
php使用exec shell命令注入的方法讲解
2013/11/12 PHP
setinterval()与clearInterval()JS函数的调用方法
2015/01/21 Javascript
Node.js node-schedule定时任务隔多少分钟执行一次的方法
2015/02/10 Javascript
Vue.js双向绑定实现原理详解
2016/12/22 Javascript
jquery实现自定义图片裁剪功能【推荐】
2017/03/08 Javascript
微信小程序 动态绑定事件并实现事件修改样式
2017/04/13 Javascript
vue引入swiper插件的使用实例
2017/07/19 Javascript
react-native-fs实现文件下载、文本存储的示例代码
2017/09/22 Javascript
echarts鼠标覆盖高亮显示节点及关系名称详解
2018/03/17 Javascript
layui导出所有数据的例子
2019/09/10 Javascript
js获取本日、本周、本月的时间代码
2020/02/01 Javascript
详解vue 组件
2020/06/11 Javascript
VUE项目实现主题切换的多种方法
2020/11/26 Vue.js
[02:50]2014DOTA2 TI预选赛预选赛 大神专访第一弹!
2014/05/21 DOTA
如何使用七牛Python SDK写一个同步脚本及使用教程
2015/08/23 Python
Python将DataFrame的某一列作为index的方法
2018/04/08 Python
Python中利用xpath解析HTML的方法
2018/05/14 Python
python实现K近邻回归,采用等权重和不等权重的方法
2019/01/23 Python
Python利用PyExecJS库执行JS函数的案例分析
2019/12/18 Python
PIL包中Image模块的convert()函数的具体使用
2020/02/26 Python
Python实现转换图片背景颜色代码
2020/04/30 Python
哈工大自然语言处理工具箱之ltp在windows10下的安装使用教程
2020/05/07 Python
Python web如何在IIS发布应用过程解析
2020/05/27 Python
keras实现VGG16 CIFAR10数据集方式
2020/07/07 Python
python和node.js生成当前时间戳的示例
2020/09/29 Python
python性能测试工具locust的使用
2020/12/28 Python
PHP面试题集
2016/12/18 面试题
数控专业个人求职信范例
2013/11/29 职场文书
美丽家庭事迹材料
2014/05/03 职场文书
2014年评职称工作总结
2014/11/20 职场文书
2015年元旦促销方案书
2014/12/09 职场文书
幽默导游词开场白
2015/05/29 职场文书
小学家庭教育心得体会
2016/01/14 职场文书
奖学金发言稿(范文)
2019/08/21 职场文书
MySQL 重写查询语句的三种策略
2021/05/10 MySQL
PyTorch 如何自动计算梯度
2021/05/23 Python