Что такое Deadlock и каковы условия его возникновения?

Ответ

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

Для возникновения Deadlock необходимо одновременное соблюдение четырех условий Коффмана:

  1. Взаимное исключение (Mutual Exclusion): Ресурс в каждый момент времени может использоваться только одним потоком.
  2. Удержание и ожидание (Hold and Wait): Поток удерживает хотя бы один ресурс и запрашивает другие ресурсы, которые в данный момент заняты другими потоками.
  3. Отсутствие вытеснения (No Preemption): Ресурс не может быть принудительно отобран у потока; он должен быть освобожден самим потоком, который его удерживает.
  4. Циклическое ожидание (Circular Wait): Существует цепочка из двух или более потоков, каждый из которых ожидает ресурс, удерживаемый следующим потоком в цепочке.

Классический пример на Python:

import threading
import time

lock_a = threading.Lock()
lock_b = threading.Lock()

def process_one():
    print("Процесс 1: Пытаюсь захватить lock_a...")
    with lock_a:
        print("Процесс 1: Захватил lock_a.")
        time.sleep(0.1) # Даем время другому потоку захватить lock_b
        print("Процесс 1: Пытаюсь захватить lock_b...")
        with lock_b: # Зависнет здесь, если lock_b захвачен Процессом 2
            print("Процесс 1: Захватил lock_b.")

def process_two():
    print("Процесс 2: Пытаюсь захватить lock_b...")
    with lock_b:
        print("Процесс 2: Захватил lock_b.")
        print("Процесс 2: Пытаюсь захватить lock_a...")
        with lock_a: # Зависнет здесь, если lock_a захвачен Процессом 1
            print("Процесс 2: Захватил lock_a.")

t1 = threading.Thread(target=process_one)
t2 = threading.Thread(target=process_two)
t1.start()
t2.start()
t1.join()
t2.join()

print("Завершено.") # Этот код никогда не выполнится

Способы предотвращения:

  • Соблюдение порядка блокировок: Все потоки должны запрашивать блокировки в одном и том же порядке (например, всегда сначала lock_a, потом lock_b).
  • Использование тайм-аутов: Захватывать блокировку с тайм-аутом (lock.acquire(timeout=1)), чтобы избежать вечного ожидания.