← Назад к вопросам

Что такое гонка чтения-записи?

2.0 Middle🔥 161 комментариев
#Архитектура систем#Базы данных и SQL

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Гонка чтения-записи (Race Condition) в многопоточных системах

Гонка чтения-записи — это класс ошибок в многопоточных (или многопроцессных) системах, которые возникают, когда несколько потоков одновременно обращаются к общему ресурсу, и результат операции зависит от порядка выполнения потоков. Это одна из самых сложных и коварных ошибок в параллельном программировании.

Как работает гонка чтения-записи

Классический пример с банковским счетом:

Представим счет с балансом 100 рублей. Два потока одновременно пытаются снять деньги:

  • Поток 1: снять 50 рублей
  • Поток 2: снять 30 рублей

Правильный результат: баланс должен быть 20 рублей

Неправильный результат из-за гонки:

Потоков 1: читает баланс (100) Поток 2: читает баланс (100) Поток 1: вычитает 50, пишет 50 Поток 2: вычитает 30, пишет 70

Результат: 70 (теряются 50 рублей!)

Типы гонок

Read-Write (Чтение-Запись):

  • Один поток читает, другой изменяет значение
  • Читающий поток видит несогласованное состояние

Write-Write (Запись-Запись):

  • Два потока одновременно пишут в один ресурс
  • Последняя запись перезаписывает предыдущую

Read-Read (Чтение-Чтение):

  • Обычно безопасно, но может быть проблема если один поток модифицирует структуру данных

Где возникают гонки

Базы данных:

  • Несколько приложений изменяют одну запись
  • Несогласованное состояние данных
  • Потеря обновлений

Веб-приложения:

  • Лайки поста (increment операция)
  • Количество доступных товаров в корзине
  • Количество просмотров статьи

Файловые системы:

  • Несколько процессов пишут в один файл
  • Микросервисы обращаются к общему хранилищу

Очереди сообщений:

  • Несколько обработчиков получают одно сообщение
  • Нужна гарантия, что сообщение обработано только один раз

Классические сценарии проблем

1. Lost Update (Потеря обновления)

Два пользователя одновременно редактируют документ:

  • User A читает версию 1
  • User B читает версию 1
  • User A пишет изменения → версия 2
  • User B пишет свои изменения → версия 2 (изменения User A потеряны)

2. Dirty Read (Грязное чтение)

Один процесс пишет неполные данные, другой их читает:

  • Процесс 1: начал транзакцию, обновил поле A
  • Процесс 2: прочитал поле A
  • Процесс 1: откатил транзакцию
  • Процесс 2 использует несуществующие данные

3. Phantom Read (Фантомное чтение)

  • Транзакция читает набор записей (WHERE id > 10)
  • Другая транзакция добавляет новую запись (id = 15)
  • Первая транзакция снова читает — появилась новая запись

Способы решения

1. Мьютексы и Блокировки (Locks)

Основной способ — сериализация доступа:

Lock lock = new Lock();

lock.acquire();
try {
  balance = balance - amount;  // критическая секция
} finally {
  lock.release();
}

Преимущества:

  • Просто и надежно
  • Гарантирует безопасность

Недостатки:

  • Снижает производительность
  • Может привести к deadlock
  • Сложно правильно использовать

2. Оптимистичная блокировка (Optimistic Locking)

Использование версий записей:

  • Чтение: SELECT balance, version FROM account WHERE id = 1
  • Изменение: UPDATE account SET balance = 50, version = 2 WHERE id = 1 AND version = 1

Если версия изменилась, другой процесс уже обновил → откат

Преимущества:

  • Лучше производительность
  • Нет deadlock

Недостатки:

  • Нужны повторные попытки
  • Не гарантирует успех при частых конфликтах

3. Пессимистичная блокировка (Pessimistic Locking)

Заранее заблокировать ресурс:

SELECT balance FROM account WHERE id = 1 FOR UPDATE;
UPDATE account SET balance = balance - 50 WHERE id = 1;

Преимущества:

  • Гарантирует успех
  • Простая логика

Недостатки:

  • Снижает производительность
  • Может привести к deadlock

4. Атомарные операции (Atomic Operations)

Использование специальных операций, которые выполняются неделимо:

balance.incrementAndGet(50);

Это одна операция на уровне CPU, без промежуточных состояний.

5. Транзакции

Все операции либо выполняются полностью, либо откатываются:

BEGIN TRANSACTION;
  UPDATE account SET balance = balance - 50 WHERE id = 1;
  UPDATE account SET balance = balance + 50 WHERE id = 2;
COMMIT;

6. Уровни изоляции транзакций

  • READ UNCOMMITTED — наименьшая безопасность
  • READ COMMITTED — средняя безопасность
  • REPEATABLE READ — хорошая безопасность
  • SERIALIZABLE — полная безопасность, но медленнее

Обнаружение и тестирование

Инструменты:

  • ThreadSanitizer (для C/C++)
  • Java Thread Analyzer
  • Stress тестирование (запустить 1000 потоков одновременно)

Практика:

  • Code review с фокусом на параллелизм
  • Unit тесты с несколькими потоками
  • Load тесты в production-подобной среде

Примеры из реальных проектов

Facebook:

  • Гонка при подсчете лайков привела к потере данных
  • Решение: оптимистичная блокировка с версионированием

Twitter:

  • Race condition при создании твита и подсчета счетчика
  • Решение: атомарные операции в кэше Redis

E-commerce:

  • Гонка при продаже последнего товара
  • Решение: SELECT FOR UPDATE перед покупкой

Лучшие практики

  • Минимизируй критические секции — блокируй только необходимое
  • Избегай вложенных блокировок — может привести к deadlock
  • Используй высокоуровневые структуры — concurrent collections вместо hand-made locks
  • Тестируй многопоточность — это не очевидно
  • Документируй shared state — явно отметь, что переменная общая
  • Используй правильный уровень изоляции — зависит от требований

Гонки чтения-записи — это фундаментальный вызов в распределенных и многопоточных системах. Правильное понимание и предотвращение этих проблем критично для надежности системы.