Взаимная блокировка (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 — взаимное вечное ожидание потоков по кругу.
- Нужны все четыре условия Коффмана; ломаем любое одно.
- Самая практичная защита — единый порядок захвата блокировок.
- Помогают таймауты и минимизация числа удерживаемых блокировок.