Перейти к содержанию
Innovialabs

Состояние гонки и взаимоблокировки: причины и защита

Продвинутое3 мин

Коротко

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

Коротко: что это и почему опасно

Состояние гонки (race condition) — ситуация, когда результат зависит от того, в каком порядке потоки или процессы выполнили операции над общими данными. Код работает на тестах и ломается под нагрузкой.

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

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

Воспроизводим гонку

Операция «прочитать — изменить — записать» не атомарна. Между чтением и записью другой поток может успеть обновить значение, и его изменение потеряется.

import threading

balance = 0

def deposit(n):
    global balance
    for _ in range(n):
        current = balance      # чтение
        balance = current + 1  # запись

threads = [threading.Thread(target=deposit, args=(100_000,)) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()
print(balance)  # может оказаться меньше 400000

В реальных системах то же самое происходит с остатками на складе, балансами и счётчиками: два запроса одновременно читают «осталось 1 шт.» и оба оформляют заказ.

Как исправить гонку

  • Блокировка (mutex, lock). Критическая секция выполняется только одним потоком.
lock = threading.Lock()

def deposit(n):
    global balance
    for _ in range(n):
        with lock:
            balance += 1
  • Атомарные операции. Для простых счётчиков есть AtomicInteger в Java, пакет sync/atomic в Go. Они быстрее мьютекса, но подходят только для одной переменной.
  • Атомарность в базе данных. Вместо чтения в коде и записи обратно пишите условие в сам запрос:
UPDATE products SET stock = stock - 1
WHERE id = 42 AND stock > 0;

Если затронуто 0 строк — товара нет. Для сложной логики используйте транзакции и SELECT ... FOR UPDATE.

  • Передача сообщений. Данными владеет один поток, остальные отправляют ему задачи через очередь (queue.Queue, каналы в Go, акторы). Нет общего изменяемого состояния — нет гонки.

Воспроизводим deadlock

import threading, time

a, b = threading.Lock(), threading.Lock()

def worker1():
    with a:
        time.sleep(0.1)
        with b: pass

def worker2():
    with b:
        time.sleep(0.1)
        with a: pass

threading.Thread(target=worker1).start()
threading.Thread(target=worker2).start()  # программа зависнет

Как избежать взаимоблокировок

  1. Единый порядок захвата. Если все потоки берут блокировки строго в порядке A, затем B, цикл ожидания невозможен. Порядок можно задать по id ресурса — например, при переводе между счетами сначала блокировать счёт с меньшим id.
  2. Таймауты. lock.acquire(timeout=1) позволяет отступить, освободить уже взятое и повторить попытку.
  3. Минимальные критические секции. Не держите блокировку во время сетевых запросов, I/O и вызовов чужого кода.
  4. Одна блокировка вместо нескольких, если производительность это позволяет.
  5. Очереди и иммутабельные данные снимают проблему на уровне архитектуры.

В базах данных deadlock тоже бывает: СУБД обнаруживает его и откатывает одну из транзакций. Код должен уметь повторить такую транзакцию.

Как обнаружить

ИнструментЧто ловит
go test -race, go run -raceгонки данных в Go
ThreadSanitizer (-fsanitize=thread)гонки в C/C++ и Rust
Дамп потоков (jstack, py-spy dump)где именно застряли потоки
Логи СУБДвзаимоблокировки транзакций
Нагрузочные тестыгонки, которые видны только при параллельных запросах

Типичные ошибки: проверять поведение одним запросом, считать операцию += атомарной, держать блокировку на время HTTP-запроса и ловить исключение о deadlock без повторной попытки.

Частые вопросы

Бывают. GIL защищает внутренние структуры интерпретатора, но поток может переключиться между чтением и записью вашей переменной. Кроме того, гонки возникают между процессами и запросами к базе.

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.