Heim >Backend-Entwicklung >Python-Tutorial >Lösen Sie das Python-Multithread-Deadlock-Problem geschickt
[Verwandte Lernempfehlungen: Python-Video]
Heute ist der 25. Artikel zum Python-Thema . Sprechen wir über das Problem des Deadlocks in der Multithread-Entwicklung.
Das Prinzip des Deadlocks ist sehr einfach , kann in einem Satz beschrieben werden. Das heißt, wenn mehrere Threads auf mehrere Sperren zugreifen, werden unterschiedliche Sperren von unterschiedlichen Threads gehalten und alle warten darauf, dass andere Threads die Sperren freigeben, sodass sie in eine permanente Wartephase geraten. Beispielsweise hält Thread A Sperre Nummer 1 und wartet auf Sperre Nummer 2, und Thread B hält Sperre Nummer 2 und wartet auf Sperre Nummer 1. Dann wird nie bis zum Tag der Ausführung gewartet. Diese Situation wird als Deadlock bezeichnet.
Es gibt ein bekanntes Deadlock-Problem namens Philosophers Dining. Es sitzen fünf Philosophen zusammen und jeder von ihnen muss zwei Gabeln bekommen, bevor er essen kann. Wenn sie gleichzeitig ihre linke Gabel aufnehmen, müssen sie ewig darauf warten, dass die rechte Gabel freigegeben wird. Dies wird zu ewigem Warten führen und diese Philosophen werden verhungern.
Dies ist ein sehr anschauliches Modell, da in Computer-Parallelitätsszenarien die Anzahl einiger Ressourcen oft begrenzt ist. Es ist sehr wahrscheinlich, dass mehrere Threads voreilen. Wenn dies nicht richtig gehandhabt wird, erhält jeder eine Ressource und wartet dann auf eine andere Ressource.
Es gibt viele Lösungen für das Deadlock-Problem. Hier stellen wir eine einfachere Lösung vor, nämlich die Nummerierung dieser Sperren. Wir legen fest, dass, wenn ein Thread mehrere Sperren gleichzeitig halten muss, auf diese Sperren in aufsteigender Reihenfolge der Seriennummern zugreifen muss. Wir können dies leicht durch Kontextmanager erreichen.
Lassen Sie uns zunächst kurz das Kontextmanagement vorstellen. Tatsächlich , wir verwenden häufig Kontextmanager. Die von uns häufig verwendete with-Anweisung ist eine klassische Verwendung von Kontextmanagern. Wenn wir eine Datei über die with-Anweisung öffnen, übernimmt sie automatisch das Schließen der Datei nach dem Lesen und die Behandlung ausgelöster Ausnahmen, wodurch wir viel Code sparen können.
Ebenso können wir auch selbst einen Kontextprozessor definieren. Es ist eigentlich sehr einfach, die beiden Funktionen __enter__ und __exit__ zu implementieren. Die Funktion __enter__ wird verwendet, um Vorgänge und Verarbeitungen vor dem Betreten der Ressource zu implementieren. Daher entspricht die Funktion __exit__ offensichtlich der Verarbeitungslogik, nachdem die Verwendung der Ressource abgeschlossen ist oder eine Ausnahme auftritt. Mit diesen beiden Funktionen haben wir unsere eigene Kontextverarbeitungsklasse.
Sehen wir uns ein Beispiel an:
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()复制代码
Wenn wir diesen Code ausführen, stimmen die auf dem Bildschirm gedruckten Ergebnisse mit unseren Erwartungen überein.
Wenn wir uns die Funktion __exit__ ansehen, werden wir feststellen, dass sie 4 Parameter hat und die drei Parameter nach übereinstimmen to ist der Fall, in dem eine Ausnahme ausgelöst wird. Typ entspricht dem Typ der Ausnahme, Wert entspricht dem Ausgabewert, wenn die Ausnahme auftritt, und Trace entspricht dem laufenden Stapel, wenn die Ausnahme ausgelöst wird. Diese Informationen werden häufig verwendet, wenn wir Ausnahmen beheben. Mithilfe dieser drei Felder können wir die Verarbeitung möglicher Ausnahmen an unsere Bedürfnisse anpassen.
Die Implementierung eines Kontextmanagers muss nicht unbedingt über eine Klasse implementiert werden. Python bietet auch Kontextverwaltungsanmerkungen. Durch die Verwendung von Annotationen können wir die Kontextverwaltung einfach implementieren. Schauen wir uns auch ein Beispiel an:
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复制代码
Bei dieser Methode entspricht der Teil vor yield der Funktion __enter__ und der Teil nach yield entspricht __exit__. Wenn eine Ausnahme auftritt, wird sie in der try-Anweisung ausgelöst. Anschließend können wir „außer“ schreiben, um die Ausnahme zu behandeln.
了解了上下文管理器之后,我们要做的就是在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()复制代码
关于死锁的问题,对锁进行排序只是其中的一种解决方案,除此之外还有很多解决死锁的模型。比如我们可以让线程在尝试持有新的锁失败的时候主动放弃所有目前已经持有的锁,比如我们可以设置机制检测死锁的发生并对其进行处理等等。发散出去其实有很多种方法,这些方法起作用的原理各不相同,其中涉及大量操作系统的基础概念和知识,感兴趣的同学可以深入研究一下这个部分,一定会对操作系统以及锁的使用有一个深刻的认识。
相关学习推荐:编程视频
Das obige ist der detaillierte Inhalt vonLösen Sie das Python-Multithread-Deadlock-Problem geschickt. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!