Короткий ответ
Нужно архитектурно свести к минимуму ситуации одновременного удержания нескольких локов, а там, где это невозможно, ввести единый документированный порядок захвата локов во всём коде и таймауты на acquire с логированием, чтобы потенциальный дедлок проявлялся как диагностируемая ошибка, а не бесконечное зависание.
Как это работает подробнее
Классический дедлок с несколькими локами требует одновременного удержания больше одного лока разными потоками в противоположном порядке; редкость и труднообъяснимость обычно означает, что условие возникает только при специфичной гонке по времени между путями кода, которые в обычных тестах не пересекаются достаточно часто.
- Компромисс. Полный отказ от нескольких одновременно удерживаемых локов устраняет риск дедлока почти полностью, но может создать излишнюю последовательность там, где логически возможен параллелизм, снижая throughput; введение таймаутов на
acquireделает дедлоки наблюдаемыми и некатастрофическими, но требует продумать обработку отказа захвата и может маскировать редкую, но настоящую архитектурную проблему — выбор зависит от того, насколько критична пропускная способность против простоты и предсказуемости системы.
Пример кода
example-286.py
import threading
# Архитектурное правило: всегда захватывать локи в порядке возрастания account.id
def transfer(acc_a, acc_b, amount):
first, second = sorted([acc_a, acc_b], key=lambda a: a.id)
got_first = first.lock.acquire(timeout=2)
if not got_first:
raise RuntimeError(f"не удалось захватить lock {first.id} за 2с - возможен deadlock")
try:
got_second = second.lock.acquire(timeout=2)
if not got_second:
raise RuntimeError(f"не удалось захватить lock {second.id} за 2с")
try:
acc_a.balance -= amount
acc_b.balance += amount
finally:
second.lock.release()
finally:
first.lock.release()Что использовать на практике
- Практика. Снижают риск архитектурно — минимизируя места, где нужно удерживать два лока одновременно, выделяя более гранулярные независимые локи для не связанных данных; там, где вложенные локи неизбежны, фиксируют единый порядок захвата как явное правило кода, проверяемое code review. В качестве защитной меры используют
acquire(timeout=...)вместо блокирующегоacquire(), логируя случаи неудачного захвата за разумное время.
Подводные камни
- Риски. Пытаются «починить» дедлок точечно, добавляя
sleepперед захватом локов — это снижает вероятность проявления в тестах, но не устраняет причину и делает проблему ещё труднее воспроизводимой; не документируют порядок захвата локов как инвариант кода, из-за чего новый код месяцы позже нарушает порядок.