Python 多线程死锁问题的巧妙解决方法
死锁
死锁的原理非常简单,用一句话就可以描述完。就是当多线程访问多个锁的时候,不同的锁被不同的线程持有,它们都在等待其他线程释放出锁来,于是便陷入了永久等待。比如A线程持有1号锁,等待2号锁,B线程持有2号锁等待1号锁,那么它们永远也等不到执行的那天,这种情况就叫做死锁。
关于死锁有一个著名的问题叫做哲学家就餐问题,有5个哲学家围坐在一起,他们每个人需要拿到两个叉子才可以吃饭。如果他们同时拿起自己左手边的叉子,那么就会永远等待右手边的叉子释放出来。这样就陷入了永久等待,于是这些哲学家都会饿死。
img
这是一个很形象的模型,因为在计算机并发场景当中,一些资源的数量往往是有限的。很有可能出现多个线程抢占的情况,如果处理不好就会发生大家都获取了一个资源,然后在等待另外的资源的情况。
对于死锁的问题有多种解决方法,这里我们介绍比较简单的一种,就是对这些锁进行编号。我们规定当一个线程需要同时持有多个锁的时候,必须要按照序号升序的顺序对这些锁进行访问。通过上下文管理器我们可以很容易实现这一点。
上下文管理器
首先我们来简单介绍一下上下文管理器,上下文管理器我们其实经常使用,比如我们经常使用的with语句就是一个上下文管理器的经典使用。当我们通过with语句打开文件的时候,它会自动替我们处理好文件读取之后的关闭以及抛出异常的处理,可以节约我们大量的代码。
同样我们也可以自己定义一个上下文处理器,其实很简单,我们只需要实现__enter__和__exit__这两个函数即可。__enter__函数用来实现进入资源之前的操作和处理,那么显然__exit__函数对应的就是使用资源结束之后或者是出现异常的处理逻辑。有了这两个函数之后,我们就有了自己的上下文处理类了。
我们来看一个样例:
classSample:
def__enter__(self):
print('enterresources')
returnself
def__exit__(self,exc_type,exc_val,exc_tb):
print('exit')
#print(exc_type)
#print(exc_val)
#print(exc_tb)
defdoSomething(self):
a=1/1
returna
defgetSample():
returnSample()
if__name__=='__main__':
withgetSample()assample:
print('dosomething')
sample.doSomething()
当我们运行这段代码的时候,屏幕上打印的结果和我们的预期是一致的。
我们观察一下__exit__函数,会发现它的参数有4个,后面的三个参数对应的是抛出异常的情况。type对应异常的类型,val对应异常时的输出值,trace对应异常抛出时的运行堆栈。这些信息都是我们排查异常的时候经常需要用到的信息,通过这三个字段,我们可以根据我们的需要对可能出现的异常进行自定义的处理。
实现上下文管理器并不一定要通过类实现,Python当中也提供了上下文管理的注解,通过使用注解我们可以很方便地实现上下文管理。我们同样也来看一个例子:
importtime
fromcontextlibimportcontextmanager
@contextmanager
deftimethis(label):
start=time.time()
try:
yield
finally:
end=time.time()
print('{}:{}'.format(label,end-start))
withtimethis('timer'):
pass
在这个方法当中yield之前的部分相当于__enter__函数,yield之后的部分相当于__exit__。如果出现异常会在try语句当中抛出,那么我们编写except对异常进行处理即可。
避免死锁
了解了上下文管理器之后,我们要做的就是在lock的外面包装一层,使得我们在获取和释放锁的时候可以根据我们的需要,对锁进行排序,按照升序的顺序进行持有。
这段代码源于Python的著名进阶书籍《Pythoncookbook》,非常经典:
fromcontextlibimportcontextmanager
#用来存储local的数据
_local=threading.local()
@contextmanager
defacquire(*locks):
#对锁按照id进行排序
locks=sorted(locks,key=lambdax:id(x))
#如果已经持有锁当中的序号有比当前更大的,说明策略失败
acquired=getattr(_local,'acquired',[])
ifacquiredandmax(id(lock)forlockinacquired)>=id(locks[0]):
raiseRuntimeError('LockOrderViolation')
#获取所有锁
acquired.extend(locks)
_local.acquired=acquired
try:
forlockinlocks:
lock.acquire()
yield
finally:
#倒叙释放
forlockinreversed(locks):
lock.release()
delacquired[-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()
defthread_1():
whileTrue:
withacquire(x_lock,y_lock):
print('Thread-1')
defthread_2():
whileTrue:
withacquire(y_lock,x_lock):
print('Thread-2')
t1=threading.Thread(target=thread_1)
t1.start()
t2=threading.Thread(target=thread_2)
t2.start()
运行一下会发现没有出现死锁的情况,但如果我们把代码稍加调整,写成这样,那么就会触发异常了。
defthread_1():
whileTrue:
withacquire(x_lock):
withacquire(y_lock):
print('Thread-1')
defthread_2():
whileTrue:
withacquire(y_lock):
withacquire(x_lock):
print('Thread-1')
因为我们把锁写成了层次结构,这样就没办法进行排序保证持有的有序性了,那么就会触发我们代码当中定义的异常。
最后我们再来看下哲学家就餐问题,通过我们自己实现的acquire函数我们可以非常方便地解决他们死锁吃不了饭的问题。
importthreading
defphilosopher(left,right):
whileTrue:
withacquire(left,right):
print(threading.currentThread(),'eating')
#叉子的数量
NSTICKS=5
chopsticks=[threading.Lock()forninrange(NSTICKS)]
forninrange(NSTICKS):
t=threading.Thread(target=philosopher,
args=(chopsticks[n],chopsticks[(n+1)%NSTICKS]))
t.start()
总结
关于死锁的问题,对锁进行排序只是其中的一种解决方案,除此之外还有很多解决死锁的模型。比如我们可以让线程在尝试持有新的锁失败的时候主动放弃所有目前已经持有的锁,比如我们可以设置机制检测死锁的发生并对其进行处理等等。发散出去其实有很多种方法,这些方法起作用的原理各不相同,其中涉及大量操作系统的基础概念和知识,感兴趣的同学可以深入研究一下这个部分,一定会对操作系统以及锁的使用有一个深刻的认识。
以上内容为大家介绍了Python多线程死锁问题的巧妙解决方法,希望对大家有所帮助,如果想要了解更多Python相关知识,请关注IT培训机构:千锋教育。
猜你喜欢LIKE
相关推荐HOT
更多>>python中的filter函数功能是什么?
python中的filter函数功能是什么?在python中,面对众多的数据,我们要过滤筛选出我们需要的数据。python中的filter函数就是起到了过滤筛选的作...详情>>
2023-11-10 20:37:27pythontime模块是什么
pythontime模块是什么在python中使用时间,就免不了和time模块打交道,另外两个模块这个暂时先不做介绍。做time模块的使用上,我们可以用它来对...详情>>
2023-11-10 15:53:16python是什么编程语言
python是什么编程语言1、说明是一种面向对象、解释型计算机程序设计语言,由GuidovanRossum于1989年底发明,第一个公开发行版发行于1991年。Pyt...详情>>
2023-11-10 15:21:05python异常处理的两种技巧
python异常处理的两种技巧1、传递异常有时我们会在捕捉到一个异常后重新引发它(传递异常),实现起来很简单,使用不带参数的raise语句即可。deff...详情>>
2023-11-10 14:49:39热门推荐
python中的filter函数功能是什么?
沸python delattr函数如何使用?
热python中pdb模块怎么用?
热Python如何截图保存?
新python中缺少module怎么办?
python strftime和strptime的不同分析
python time.strptime的格式化
python中@contextmanager是什么?
python对象的三要素是什么
pythonGIL在Python多线程的应用
python如何对多个CSV文件进行读取
pythonif嵌套命令如何理解?
python对列表进行永久性或临时排序的方法
python生成器调用方法引发异常