Получить доступ

Что такое состояние гонки?

JuniorКонкурентность
Ещё спрашивают так:
  • Почему count++ из двух потоков даёт неверный результат?
  • Что такое критическая секция?
  • Чем race condition отличается от data race?
Коротко

Гонка — результат зависит от того, в каком порядке потоки выполнили свои шаги. Классика: count++ — это три шага (прочитать, прибавить, записать), и два потока теряют обновления.

  • Критическая секция — участок, который должен выполняться одним потоком за раз
  • Решения: блокировка, атомарные операции, неизменяемые данные, отказ от общего состояния
  • «Проверить, потом действовать» (check-then-act) — гонка, даже если каждая операция атомарна

А ещё Гонки плохо воспроизводятся: тесты проходят, прод падает раз в неделю

Так подсказку видит пользователь sotto на живом собеседовании — за пару секунд, пока вы думаете вслух.

Попробуйте сами

Взаимная блокировка двух потоков

Подробно

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

Потерянное обновление. count++ компилируется в чтение значения в регистр, прибавление и запись обратно. Если оба потока прочитали 5, оба запишут 6 — одно увеличение потерялось. Десять потоков по миллиону инкрементов дадут заметно меньше десяти миллионов.

Check-then-act. «Если ключа нет в кэше — посчитать и положить», «если на счёте хватает денег — списать». Между проверкой и действием другой поток может изменить состояние. Даже потокобезопасная коллекция не спасает, если проверка и действие — два отдельных вызова: нужны атомарные составные операции (computeIfAbsent, putIfAbsent) или блокировка вокруг обоих.

Data race и race condition. Строго говоря, data race — одновременный доступ к одной ячейке памяти без синхронизации, где хотя бы один доступ — запись. Это ошибка на уровне модели памяти языка (в Java — нет гарантий видимости, в C++ — неопределённое поведение). Race condition шире — логическая ошибка порядка, она бывает и без data race (две атомарные операции в неудачном порядке) и даже между процессами или сервисами: два запроса одновременно бронируют последнее место.

Как устраняют:

  • взаимное исключение — мьютекс, synchronized, Lock вокруг критической секции;
  • атомарные операции — AtomicInteger.incrementAndGet, CAS;
  • неизменяемость — нечего гонять, если данные не меняются;
  • изоляция — у каждого потока свои данные (ThreadLocal, акторы, обмен сообщениями вместо общей памяти);
  • в базе — транзакции с нужной изоляцией, SELECT ... FOR UPDATE, оптимистическая блокировка по версии, атомарный UPDATE ... SET x = x - 1 WHERE x > 0.

Пример

import threading
count = 0
def work():
    global count
    for _ in range(100_000):
        count += 1          # прочитать, прибавить, записать — не атомарно

ts = [threading.Thread(target=work) for _ in range(8)]
[t.start() for t in ts]; [t.join() for t in ts]
print(count)                # может быть меньше 800000

lock = threading.Lock()
def safe_work():
    global count
    for _ in range(100_000):
        with lock:          # критическая секция
            count += 1

Что хотят услышать

  • Определение через зависимость от порядка выполнения.
  • Разбор count++ на шаги и потерянное обновление.
  • Check-then-act как отдельный вид гонки.
  • Способы устранения, включая неизменяемость и решения в базе.

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

  • Думать, что потокобезопасная коллекция делает потокобезопасным весь код вокруг неё.
  • Проверять отсутствие гонки одним запуском теста.

Что спросят дальше

Связанные вопросы

Закрытая бета

Подсказки на собеседовании — в реальном времени

sotto слышит вопрос интервьюера и показывает короткий ответ и план, как в блоке «Коротко». Оставьте контакт — пришлём доступ и промокод.

  • Промокод на 2 часа — сверх пробных 30 минут
  • Программа для Windows — скачать сразу, войти — через Яндекс ID или VK ID
  • Прямая связь с разработчиком: что улучшить — сделаем