ホームページ  >  記事  >  バックエンド開発  >  Pythonのマルチスレッドデッドロック問題を賢く解決する

Pythonのマルチスレッドデッドロック問題を賢く解決する

coldplay.xixi
coldplay.xixi転載
2020-08-31 17:09:081750ブラウズ

[関連する学習に関する推奨事項: Python ビデオ ]

Python 特集 の第 25 回目となる今日は、マルチスレッド開発におけるデッドロックの問題についてお話します。

デッドロック

デッドロックの原理は非常に単純です、一文で説明できます。つまり、複数のスレッドが複数のロックにアクセスすると、

異なるロックが異なるスレッドによって保持され、それらはすべて他のスレッドがロックを解放するのを待っているため、永続的な待機状態に陥ります。たとえば、スレッド A はロック番号 1 を保持してロック番号 2 を待機し、スレッド B はロック番号 2 を保持してロック番号 1 を待機します。この場合、スレッドは実行日まで待機することはありません。この状況はデッドロックと呼ばれます。 デッドロックに関する有名な問題として、

哲学者の食事

問題というものがあります。5 人の哲学者が一緒に座っており、食事をする前に各人が 2 本のフォークを用意する必要があります。同時に左手のフォークを手に取ると、右手のフォークが解放されるまで永遠に待つことになります。これは永遠の待ち時間につながり、これらの哲学者たちは餓死するでしょう。

Pythonのマルチスレッドデッドロック問題を賢く解決する
コンピュータの同時実行シナリオでは、一部の
リソースの数が制限されることが多いため、これは非常に鮮明なモデルです

。複数のスレッドがプリエンプトする可能性が非常に高く、適切に処理しないと、全員がリソースを取得した後、別のリソースを待機することになります。 デッドロックの問題には多くの解決策がありますが、ここでは、ロックに番号を付けるという、より簡単な解決策を紹介します。スレッドが同時に複数のロックを保持する必要がある場合、

はシリアル番号

の昇順でこれらのロックにアクセスする必要があると規定しています。これは、コンテキスト マネージャーを使用して簡単に実現できます。

コンテキスト マネージャー

まず、コンテキスト管理について簡単に紹介します。たとえば、私たちがよく使う with ステートメント は、コンテキスト マネージャーの古典的な使用法です。 with ステートメントを使用してファイルを開くと、読み取り後のファイルのクローズと、スローされた例外の処理が自動的に処理されるため、多くのコードを節約できます。

同様に、コンテキスト プロセッサを自分で定義することもできます。実際には非常に簡単です。実装する必要があるのは __enter__ と __exit__ という 2 つの関数だけです。 __enter__ 関数は、リソースに入る前の操作や処理を実装するために使用されます。したがって、__exit__ 関数は、リソースの使用が完了したり、例外が発生した後の処理ロジックに相当します。これら 2 つの関数を使用して、独自のコンテキスト処理クラスを作成します。

例を見てみましょう:

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()复制代码

このコードを実行すると、画面に表示される結果は期待どおりです。

Pythonのマルチスレッドデッドロック問題を賢く解決する

__exit__ 関数を見ると、4 つのパラメーターがあり、 の後の 3 つのパラメーターが対応していることがわかります。 to は、例外がスローされるケースです。 type は例外のタイプに対応し、val は例外が発生したときの出力値に対応し、trace は例外がスローされたときの実行中のスタックに対応します。この情報は、例外のトラブルシューティングを行うときによく使用され、これら 3 つのフィールドを通じて、必要に応じて考えられる例外の処理をカスタマイズできます。

コンテキスト マネージャーの実装は、必ずしもクラスで実装する必要はありません。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 ステートメントでスローされ、例外を処理するための例外を記述できます。

避免死锁

了解了上下文管理器之后,我们要做的就是在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のマルチスレッドデッドロック問題を賢く解決するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はjuejin.imで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。