Взаимная блокировка (deadlock) и условия Коффмана

Когда два потока вежливо ждут друг друга — и оба зависают навсегда.

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

Классический сценарий: поток A захватил блокировку 1 и хочет блокировку 2, а поток B захватил блокировку 2 и хочет блокировку 1. Оба ждут — и никто не отпустит, потому что ждёт сам.

Схема смертельных объятий

Поток A                 Поток B
захватил lock1          захватил lock2
ждёт   lock2  <----+    ждёт   lock1
                   |              |
     держит lock1 --+--> нужен A  |
     держит lock2 <------ нужен B -+

Круг ожидания замкнулся -> deadlock

Четыре условия Коффмана

Deadlock возможен только когда выполнены все четыре условия одновременно (Coffman, 1971):

УсловиеСмысл
Взаимное исключениересурс нельзя делить — он у одного
Удержание и ожиданиепоток держит один ресурс и ждёт другой
Отсутствие вытесненияресурс нельзя отобрать силой
Круговое ожиданиеесть цикл потоков, ждущих друг друга

Чтобы предотвратить deadlock, достаточно сломать любое одно условие. Самый практичный способ — убрать круговое ожидание, всегда захватывая блокировки в одном и том же порядке.

Упорядочивание блокировок

lock_order = {"lock1": 1, "lock2": 2}

def safe_acquire(a, b):
    # всегда берём блокировки по возрастанию номера
    first, second = sorted([a, b], key=lambda x: lock_order[x])
    print(f"беру {first}, затем {second}")

safe_acquire("lock2", "lock1")
safe_acquire("lock1", "lock2")

Вывод:

беру lock1, затем lock2
беру lock1, затем lock2

Оба потока теперь берут lock1 первым, поэтому цикл ожидания невозможен.

Как работает под капотом

Операционные системы и СУБД умеют обнаруживать deadlock, строя граф ожидания ресурсов: если в графе есть цикл — это тупик. СУБД при обнаружении выбирает «жертву» и откатывает её транзакцию. В обычном прикладном коде встроенного детектора нет, поэтому защищаются профилактикой: единый порядок блокировок, таймауты на захват (acquire(timeout=...)), отказ от удержания нескольких блокировок сразу.

Частые ошибки

  • Брать вложенные блокировки в разном порядке. Это прямая дорога к deadlock.
  • Удерживать блокировку во время долгой операции (I/O). Растёт окно, в котором случится тупик.
  • Не ставить таймаут. Без него тупик висит вечно и его трудно диагностировать.

Итог

  • Deadlock — взаимное вечное ожидание потоков по кругу.
  • Нужны все четыре условия Коффмана; ломаем любое одно.
  • Самая практичная защита — единый порядок захвата блокировок.
  • Помогают таймауты и минимизация числа удерживаемых блокировок.
Проверьте себя
1. Сколько условий Коффмана должно выполняться одновременно, чтобы возник deadlock?
AЛюбое одно
BВсе четыре
CРовно два
DМинимум пять
2. Какой приём практичнее всего предотвращает круговое ожидание?
AСоздавать больше потоков
BВсегда захватывать блокировки в одном и том же порядке
CУбрать все блокировки
DУвеличить кванты времени