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进程管理工具supervisor使用实例
Sep 17 Python
Python字符串格式化
Jun 15 Python
python3编码问题汇总
Sep 06 Python
利用python获取某年中每个月的第一天和最后一天
Dec 15 Python
基于python3 类的属性、方法、封装、继承实例讲解
Sep 19 Python
Python数据结构与算法之图的最短路径(Dijkstra算法)完整实例
Dec 12 Python
Python实现判断并移除列表指定位置元素的方法
Apr 13 Python
Python随机数函数代码实例解析
Feb 09 Python
浅谈tensorflow中dataset.shuffle和dataset.batch dataset.repeat注意点
Jun 08 Python
Python配置pip国内镜像源的实现
Aug 20 Python
python中用ctypes模拟点击的实例讲解
Nov 26 Python
python爬取股票最新数据并用excel绘制树状图的示例
Mar 01 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
mysql 字段类型说明
2007/04/27 PHP
destoon实现资讯信息前面调用它所属分类的方法
2014/07/15 PHP
PHP实现AES256加密算法实例
2014/09/22 PHP
PHP输出一个等腰三角形的方法
2015/05/12 PHP
php实现图片缩略图的方法
2016/03/29 PHP
基于jquery的兼容各种浏览器的iframe自适应高度的脚本
2010/08/13 Javascript
Chosen 基于jquery的选择框插件使用方法
2012/05/30 Javascript
跟我学Nodejs(二)--- Node.js事件模块
2014/05/21 NodeJs
基于AngularJS实现页面滚动到底自动加载数据的功能
2015/10/16 Javascript
详解AngularJS过滤器的使用
2016/03/11 Javascript
javascript函数中的3个高级技巧
2016/09/22 Javascript
Vue.JS入门教程之自定义指令
2016/12/08 Javascript
JS给按钮添加跳转功能类似a标签
2017/05/30 Javascript
Node.js+jade+mongodb+mongoose实现爬虫分离入库与生成静态文件的方法
2017/09/20 Javascript
js实现简单数字变动效果
2017/11/06 Javascript
React学习之JSX与react事件实例分析
2020/01/06 Javascript
Vue项目中使用flow做类型检测的方法
2020/03/18 Javascript
详解javascript void(0)
2020/07/13 Javascript
解决在Vue中使用axios POST请求变成OPTIONS的问题
2020/08/14 Javascript
简单介绍Python中的JSON使用
2015/04/28 Python
pycharm激活码有效到2020年11月底
2020/09/18 Python
Python 程序员必须掌握的日志记录
2020/08/17 Python
如何通过python实现IOU计算代码实例
2020/11/02 Python
python简单实现插入排序实例代码
2020/12/16 Python
HTML5 DeviceOrientation实现手机网站摇一摇功能代码实例
2015/04/24 HTML / CSS
美国维生素、补充剂、保健食品购物网站:Vitacost
2016/08/05 全球购物
加拿大折扣、优惠券和交易网站:WagJag
2018/02/07 全球购物
俄罗斯电子产品、计算机和家用电器购物网站:OLDI
2019/10/27 全球购物
企业为何需要商业计划书
2013/12/26 职场文书
城管大队整治方案
2014/05/06 职场文书
房地产推广策划方案
2014/05/19 职场文书
初中生旷课检讨书范文
2014/10/06 职场文书
导游欢送词
2015/01/31 职场文书
浅析Django接口版本控制
2021/06/26 Python
SpringBoot2零基础到精通之数据与页面响应
2022/03/22 Java/Android
Node.js实现爬取网站图片的示例代码
2022/04/04 NodeJs