Python | 多线程死锁问题的巧妙解决方法
本文始发于个人公众号:TechFlow,原创不易,求个关注
今天是Python专题的第25篇文章,我们一起来聊聊多线程开发当中死锁的问题。
死锁
死锁的原理非常简单,用一句话就可以描述完。就是当多线程访问多个锁的时候,不同的锁被不同的线程持有,它们都在等待其他线程释放出锁来,于是便陷入了永久等待。比如A线程持有1号锁,等待2号锁,B线程持有2号锁等待1号锁,那么它们永远也等不到执行的那天,这种情况就叫做死锁。
关于死锁有一个著名的问题叫做哲学家就餐问题,有5个哲学家围坐在一起,他们每个人需要拿到两个叉子才可以吃饭。如果他们同时拿起自己左手边的叉子,那么就会永远等待右手边的叉子释放出来。这样就陷入了永久等待,于是这些哲学家都会饿死。

这是一个很形象的模型,因为在计算机并发场景当中,一些资源的数量往往是有限的。很有可能出现多个线程抢占的情况,如果处理不好就会发生大家都获取了一个资源,然后在等待另外的资源的情况。
对于死锁的问题有多种解决方法,这里我们介绍比较简单的一种,就是对这些锁进行编号。我们规定当一个线程需要同时持有多个锁的时候,必须要按照序号升序的顺序对这些锁进行访问。通过上下文管理器我们可以很容易实现这一点。
上下文管理器
首先我们来简单介绍一下上下文管理器,上下文管理器我们其实经常使用,比如我们经常使用的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()
当我们运行这段代码的时候,屏幕上打印的结果和我们的预期是一致的。

我们观察一下__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 | 多线程死锁问题的巧妙解决方法的更多相关文章
- $ sudo python -m pip install pylint 出错解决方法
问题:在unbuntu执行$ sudo python -m pip install pylint出错解决方法支行以下命令sudo pip install pylint==1.9.3这样roboware ...
- python用户评论标签匹配的解决方法
python用户评论标签匹配的解决方法 这篇文章主要为大家详细介绍了python用户评论标签匹配的解决方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下 我们观察用户评论发现:属性词往往和情感词伴 ...
- Python实现全局变量的两个解决方法
Python实现全局变量的两个解决方法 本文针对Python的全局变量实现方法简述如下: 先来看下面一段测试程序: count = 0 def Fuc(count): print coun ...
- win安装python模块出现依赖问题的解决方法 & No module named 'MySqldb'
前言 一年多了,还在写这种问题,羞愧. 新公司不让用自己的电脑,配的winPC,项目启不起来,之前也出现过这个问题,是py3缺少某个模块,但是自己没记,这次记一下好了. No module named ...
- 【java 多线程】多线程并发同步问题及解决方法
一.线程并发同步概念 线程同步其核心就在于一个“同”.所谓“同”就是协同.协助.配合,“同步”就是协同步调昨,也就是按照预定的先后顺序进行运行,即“你先,我等, 你做完,我再做”. 线程同步,就是当线 ...
- MSSQL死锁产生原因及解决方法
一. 什么是死锁 死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去.此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等的进 ...
- 【Python】Non-ASCII character '\xe6' 错误解决方法
刚刚在写Python程序的时候遇到了一个问题,无论是在程序中什么地方出现中文字符,都会出现如下错误 SyntaxError: Non-ASCII character '\xe6' 网上查阅了一下这应该 ...
- python积累二:中文乱码解决方法
根据网上提供的解决方法:添加#coding=utf-8或# -*- coding: utf-8 -*- #coding=utf-8 print "还不行?" 执行结果:还是乱码!: ...
- MySQL死锁问题分析及解决方法实例详解(转)
出处:http://www.jb51.net/article/51508.htm MySQL死锁问题是很多程序员在项目开发中常遇到的问题,现就MySQL死锁及解决方法详解如下: 1.MySQL常用 ...
随机推荐
- Day03_破解Windows7系统密码&用户与组管理&服务器远程管理
破解Windows系统密码 一.利用5次shift漏洞破解win7密码 1.1 漏洞 1.在未登录系统时,连续按5次shift键,弹出程序c:\windows\system32\sethc.exe 2 ...
- 那些年拿过的shell之springboot jolokia rce
日穿扫描扫到一个spring boot actuator 可以看到有jolokia这个端点,再看下jolokia/list,存在type=MBeanFactory 关键字 可以使用jolokia-re ...
- __slots__属性
使用__slots__时,子类不受影响 class Person(object): __slots__ = ("name","age") def __str__ ...
- Python os.chown() 方法
概述 os.chown() 方法用于更改文件所有者,如果不修改可以设置为 -1, 你需要超级用户权限来执行权限修改操作.高佣联盟 www.cgewang.com 只支持在 Unix 下使用. 语法 c ...
- bzoj 1195 [HNOI2006]最短母串 bfs 状压 最短路 AC自动机
LINK:最短母串 求母串的问题.不适合SAM. 可以先简化问题 考虑给出的n个字符串不存在包含关系. 那么 那么存在的情况 只可能有 两个字符串拼接起来能表示另外一个字符串 或者某个字符串的后缀可以 ...
- js数组中如何去除重复值?
在日常开发中,我们可能会遇到将一个数组中里面的重复值去除,那么,我就将我自己所学习到的几种方法分享出来 去除数组重复值方法: 1,利用indexOf()方法去除 思路:创建一个新数组,然后循环要去重的 ...
- 【NOIP2015】斗地主 题解(DFS+贪心)
题目描述 牛牛最近迷上了一种叫斗地主的扑克游戏.斗地主是一种使用黑桃.红心.梅花.方片的AAA到KKK加上大小王的共545454张牌来进行的扑克牌游戏.在斗地主中,牌的大小关 系根据牌的数码表示如下: ...
- Chrome太占内存?试试这个
" The Great Suspender" 是一个免费的开源 Google Chrome 扩展程序,适用于那些发现chrome占用过多系统资源或经常遭受chrome崩溃的人. 一 ...
- JS学习第二天
数组: var arr1=[2,5,6]; 定义时直接给数组元素赋值 var arr2=[]; 定义一个空数组 var arr3=new Array(); 定义一个空数组并通过索引来 ...
- java 的API及Object类
一 Java的API Java 的API(API: Application(应用) Programming(程序) Interface(接口)) Java API就是JDK中提供给我们使用的类,这些类 ...