Python | Умное решение проблемы многопоточной взаимоблокировки

Python

Эта статья возникла из личного публичного аккаунта:TechFlow, оригинальность это не просто, прошу внимания


СегодняТемы в PythonВ 25-й статье поговорим о взаимоблокировках в многопоточной разработке.

тупик

Принцип взаимоблокировки очень прост и может быть описан одним предложением. То есть, когда несколько потоков обращаются к нескольким блокировкам,Разные замки удерживаются разными потоками, все они ждут, пока другие потоки снимут блокировку, поэтому они находятся в постоянном ожидании. Например, поток A держит блокировку №1 и ждет блокировки №2, а поток B держит блокировку №2 и ждет блокировки №1, тогда они никогда не будут ждать дня исполнения, такая ситуация называется тупиковой. .

Существует известный вопрос о взаимоблокировках, который называетсяФилософская столоваяПроблема, сидят 5 философов, и каждому из них нужно получить две вилки, чтобы поесть. Если бы они взяли свою левую вилку одновременно, они бы вечно ждали, пока освободится правая вилка. Это вечное ожидание, и эти философы умрут с голоду.

img
img

Это очень наглядная модель, потому что в сценариях параллелизма на компьютере некоторыеКоличество ресурсов часто ограничено. Очень вероятно, что несколько потоков упредят ситуацию.Если это не будет обработано должным образом, случится так, что каждый получит ресурс, а затем будет ждать другого ресурса.

Есть много решений проблемы взаимоблокировки, здесь мы представляем относительно простое решение, состоящее в том, чтобы пронумеровать эти блокировки. Мы оговариваем, что когда потоку необходимо удерживать несколько блокировок одновременно,Доступ к этим замкам должен осуществляться в порядке возрастания серийных номеров.. Мы можем легко добиться этого с помощью контекстных менеджеров.

менеджер контекста

Прежде всего, давайте кратко познакомимся с менеджером контекста.На самом деле мы часто используем диспетчер контекста, например, тот, который мы часто используем.с заявлениемЭто классическое использование менеджера контекста. Когда мы открываем файл с помощью оператора 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 a

def getSample():
    return Sample()

if __name__ == '__main__':
    with getSample() as sample:
        print('do something')
        sample.doSomething()

Когда мы запускаем этот код, результаты, напечатанные на экране, соответствуют нашим ожиданиям.

image-20200803091558632
image-20200803091558632

Давайте посмотрим на функцию __exit__ и обнаружим, что она имеет 4 параметра,Последние три параметра соответствуют случаю, когда выбрасывается исключение. type соответствует типу исключения, val соответствует выходному значению при возникновении исключения, а trace соответствует работающему стеку при возникновении исключения. Эта информация является информацией, которую нам часто нужно использовать при устранении неполадок исключений.С помощью этих трех полей мы можем настроить обработку возможных исключений в соответствии с нашими потребностями.

Реализация менеджеров контекста не обязательно должна быть реализована через классы. Python также предоставляет аннотации управления контекстом. Используя аннотации, мы можем легко реализовать управление контекстом. Давайте также рассмотрим пример:

import time
from contextlib import contextmanager

@contextmanager
def 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 возникает исключение, мы можем написать, за исключением обработки исключения.

избежать тупика

Теперь, когда мы понимаем менеджеров контекста, все, что нам нужно сделать, этоОберните слой снаружи замка, так что когда мы получаем и освобождаем блокировки, мы можем сортировать блокировки в соответствии с нашими потребностями и удерживать их в порядке возрастания.

Этот код взят из известной продвинутой книги Python "Python cookbook", которая очень классическая:

from contextlib import contextmanager

# 用来存储local的数据
_local = threading.local()

@contextmanager
def 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этот компонент.

Это в многопоточном сценарииобщая переменная, несмотря на то, что он является общим, значение, читаемое каждым потоком, независимо. Звучит немного сложно для понимания, но на самом деле мы можем понимать это как словарь.Ключом словаря является идентификатор каждого потока, а значением является словарь, в котором хранятся данные. Когда каждый поток обращается к локальной переменной, это эквивалентно сначала получению независимого словаря через идентификатор потока, а затем работе с этим словарем.

Кажется, что мы используем _local напрямую, когда используем его, потому что шаг запроса сначала по идентификатору потока инкапсулирован в нем. Если вы этого не понимаете, вам может быть трудно понять.

Давайте еще раз посмотрим на использование этого приобретения:

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')

Поскольку мы записали блокировку как иерархическую структуру, нет возможности отсортировать ее, чтобы обеспечить порядок удержания, тогда она вызовет исключение, определенное в нашем коде.

Наконец, давайте взглянем на обеденную проблему философа.С помощью нашей собственной реализации функции приобретения мы можем легко решить проблему их тупиковой ситуации, когда они не могут есть.

import threading

def philosopher(left, right):
    while True:
        with acquire(left,right):
             print(threading.currentThread(), 'eating')

# 叉子的数量
NSTICKS = 5
chopsticks = [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()

Суммировать

О взаимоблокировках, блокировках сортировкипросто одно из решений, в дополнение ко многим моделям для решения взаимоблокировок. Например, мы можем позволить потокам отказываться от всех удерживаемых в настоящее время блокировок, когда они пытаются удерживать новые блокировки и терпят неудачу, например, мы можем настроить механизмы для обнаружения возникновения взаимоблокировок и их устранения и т. д. На самом деле способов разброса много, и принципы этих методов разные.Они предполагают много базовых понятий и знаний об операционных системах.Заинтересованные студенты могут изучить эту часть углубленно,и они точно будут хорошо разбираться использования операционных систем и замков глубокое понимание.

Сегодняшняя статья здесь, если вам понравилась эта статья, пожалуйста, приходите на волнукачество три, поддержите меня (Подписывайтесь, делайте репосты, лайкайте).

- END -

Отсканируйте код, чтобы следовать, чтобы получить больше статей