巧妙解决Python多线程死锁问题-Python教程

资源魔 33 0

【相干学习保举:python视频】

明天是Python专题的第25篇文章,咱们一同来聊聊多线程开发傍边死锁的成绩。

死锁

死锁的原理十分简略,用一句话就能够形容完。就是当多线程拜访多个锁的时分,没有同的锁被没有同的线程持有,它们都正在期待其余线程开释出锁来,于是便堕入了永世期待。比方A线程持有1号锁,期待2号锁,B线程持有2号锁期待1号锁,那末它们永远也等没有到执行的那天,这类状况就叫做死锁。

对于死锁有一个驰名的成绩叫做哲学家就餐成绩,有5个哲学家围坐正在一同,他们每一个人需求拿到两个叉子才能够用饭。假如他们同时拿起本人左手边的叉子,那末就会永远期待右手边的叉子开释进去。这样就堕入了永世期待,于是这些哲学家城市饿死。

img

这是一个很抽象的模子,由于正在较量争论机并发场景傍边,一些资本的数目往往是无限的。颇有可能呈现多个线程抢占的状况,假如解决欠好就会发作各人都猎取了一个资本,而后正在期待另外的资本的状况。

关于死锁的成绩有多种处理办法,这里咱们引见比拟简略的一种,就是对这些锁进行编号。咱们规则当一个线程需求同时持有多个锁的时分,必需要依照序号升序的程序对这些锁进行拜访。经过上下文治理器咱们能够很容易完成这一点。

上下文治理器

起首咱们来简略引见一下上下文治理器,上下文治理器咱们其实常常应用,比方咱们常常应用的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 adef getSample():    return Sample()if __name__ == '__main__':    with getSample() as sample:        print('do something')        sample.doSomething()复制代码

当咱们运转这段代码的时分,屏幕上打印的后果以及咱们的预期是分歧的。

image-20200803091558632

咱们察看一下__exit__函数,会发现它的参数有4个,前面的三个参数对应的是抛出异样的状况。type对应异样的类型,val对应异样时的输入值,trace对应异样抛出时的运转货仓旅馆。这些信息都是咱们排查异样的时分常常需求用到的信息,经过这三个字段,咱们能够依据咱们的需求对可能呈现的异样进行自界说的解决。

完成上下文治理器其实不肯定要经过类完成,Python傍边也提供了上下文治理的注解,经过应用注解咱们能够很不便地完成上下文治理。咱们一样也来看一个例子:

import timefrom contextlib import contextmanager@contextmanagerdef 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()@contextmanagerdef 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 threadingdef philosopher(left, right):    while True:        with acquire(left,right):             print(threading.currentThread(), 'eating')# 叉子的数目NSTICKS = 5chopsticks = [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编程 python使用问题 死锁

抱歉,评论功能暂时关闭!