千锋教育-做有情怀、有良心、有品质的职业教育机构

400-811-9990
手机站
千锋教育

千锋学习站 | 随时随地免费学

千锋教育

扫一扫进入千锋手机站

领取全套视频
千锋教育

关注千锋学习站小程序
随时随地免费学习课程

上海
  • 北京
  • 郑州
  • 武汉
  • 成都
  • 西安
  • 沈阳
  • 广州
  • 南京
  • 深圳
  • 大连
  • 青岛
  • 杭州
  • 重庆
当前位置:太原千锋IT培训  >  技术干货  >  Python 多线程死锁问题的巧妙解决方法

Python 多线程死锁问题的巧妙解决方法

来源:千锋教育
发布人:xqq
时间: 2023-11-08 19:35:55

死锁

死锁的原理非常简单,用一句话就可以描述完。就是当多线程访问多个锁的时候,不同的锁被不同的线程持有,它们都在等待其他线程释放出锁来,于是便陷入了永久等待。比如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

Python 常见的内置函数

2023-11-08

python文件读写实现原理与操作步骤

2023-11-08

python的十个第三方库

2023-11-08

最新文章NEW

Python内置的列表

2023-11-08

python np.nonzero()函数

2023-11-08

python pickle模块

2023-11-08

相关推荐HOT

更多>>

快速通道 更多>>

最新开班信息 更多>>

网友热搜 更多>>