>  기사  >  백엔드 개발  >  Python 다중 스레드 교착 상태 문제를 영리하게 해결

Python 다중 스레드 교착 상태 문제를 영리하게 해결

coldplay.xixi
coldplay.xixi앞으로
2020-08-31 17:09:081750검색

【관련 학습 추천: python video

오늘은 Python Special Topic 25번째 글입니다.

Deadlock

교착상태의 원리는 매우 간단하며 한 문장으로 설명할 수 있습니다. 즉, 여러 스레드가 여러 잠금에 액세스할 때 다른 스레드가 다른 잠금을 보유하고, 모두 다른 스레드가 잠금을 해제하기를 기다리고 있으므로 영구 대기 상태에 빠지게 됩니다. 예를 들어 스레드 A는 잠금 번호 1을 보유하고 잠금 번호 2를 기다리고 있으며 스레드 B는 잠금 번호 2를 보유하고 잠금 번호 1을 기다리고 있습니다. 그러면 스레드는 실행일까지 결코 기다리지 않습니다. 이 상황을 교착 상태라고 합니다.

교착 상태에 관한 유명한 문제인 철학자 식사문제가 있습니다. 5명의 철학자가 함께 앉아 있는데, 각자 포크 두 개를 가져와야 식사를 할 수 있습니다. 동시에 왼쪽 포크를 집으면 오른쪽 포크가 놓일 때까지 영원히 기다리게 됩니다. 이것은 영원한 기다림으로 이어질 것이며, 이 철학자들은 굶어 죽게 될 것입니다.

Python 다중 스레드 교착 상태 문제를 영리하게 해결

이것은 매우 생생한 모델입니다. 컴퓨터 동시성 시나리오에서는 일부 리소스의 수가 제한되는 경우가 많기 때문입니다. 여러 스레드가 선점될 가능성이 매우 높습니다. 제대로 처리되지 않으면 모든 사람이 리소스를 얻은 다음 다른 리소스를 기다릴 것입니다.

교착 상태 문제에 대한 해결책은 다양합니다. 여기서는 이러한 잠금에 번호를 매기는 더 간단한 방법을 소개합니다. 스레드가 동시에 여러 잠금을 보유해야 하는 경우 일련 번호의 오름차순으로 이러한 잠금에 액세스해야 한다고 규정합니다. 컨텍스트 관리자를 통해 이를 쉽게 달성할 수 있습니다.

컨텍스트 관리자

먼저 컨텍스트 관리자를 간단히 소개하겠습니다. 예를 들어 우리가 자주 사용하는 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()复制代码

이 코드를 실행하면 화면에 인쇄된 결과가 우리의 기대와 일치합니다.

Python 다중 스레드 교착 상태 문제를 영리하게 해결

__exit__ 함수를 보면 4개의 매개변수가 있는 것을 알 수 있습니다. 마지막 3개의 매개변수는 예외가 발생하는 상황에 해당합니다. 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 다중 스레드 교착 상태 문제를 영리하게 해결의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 juejin.im에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제