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

Что такое состояние гонки (Race condition)?

1.8 Middle🔥 151 комментариев
#Асинхронность и многопоточность

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

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

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

Что такое состояние гонки (Race condition)?

Состояние гонки (Race condition) — это ошибка параллельного программирования, которая возникает, когда несколько потоков или процессов одновременно получают доступ к общему ресурсу (переменной, файлу, базе данных) и изменяют его без синхронизации. Результат выполнения становится зависимым от временной последовательности выполнения потоков, что приводит к непредсказуемому поведению.

Простой пример Race Condition

import threading

# Общий ресурс
balance = 1000

def withdraw(amount):
    global balance
    # Эти операции НЕ атомарны!
    temp = balance  # Шаг 1: Прочитать
    temp -= amount  # Шаг 2: Вычесть
    balance = temp  # Шаг 3: Записать

# Создаем два потока, каждый снимает 100
thread1 = threading.Thread(target=withdraw, args=(100,))
thread2 = threading.Thread(target=withdraw, args=(100,))

thread1.start()
thread2.start()
thread1.join()
thread2.join()

print(f"Balance: {balance}")  # Ожидаем: 800, получим: 900!
# Почему? Оба потока прочитали 1000, вычли по 100, записали 900

Как это происходит?

Рассмотрим временную последовательность:

Время  | Поток 1           | Поток 2           | balance
-------|-------------------|-------------------|----------
1      | temp1 = 1000      |                   | 1000
2      |                   | temp2 = 1000      | 1000
3      | temp1 = 1000-100  |                   | 1000
4      |                   | temp2 = 1000-100  | 1000
5      | balance = 900     |                   | 900
6      |                   | balance = 900     | 900 ← ОШибка!

Решение 1: Mutex (Lock)

import threading

balance = 1000
lock = threading.Lock()

def withdraw(amount):
    global balance
    with lock:  # Захватываем блокировку
        temp = balance
        temp -= amount
        balance = temp
        # Блокировка автоматически освобождается

thread1 = threading.Thread(target=withdraw, args=(100,))
thread2 = threading.Thread(target=withdraw, args=(100,))

thread1.start()
thread2.start()
thread1.join()
thread2.join()

print(f"Balance: {balance}")  # Теперь правильно: 800

Как это работает:

Время  | Поток 1                | Поток 2           | balance
-------|------------------------|-------------------|----------
1      | lock.acquire()         |                   | 1000
2      | temp1 = 1000           |                   | 1000
3      | temp1 = 1000-100       |                   | 1000
4      | balance = 900          |                   | 900
5      | lock.release()         |                   | 900
6      |                        | lock.acquire()    | 900
7      |                        | temp2 = 900       | 900
8      |                        | temp2 = 900-100   | 900
9      |                        | balance = 800     | 800 ✓
10     |                        | lock.release()    | 800

Решение 2: Атомарные операции

import threading
from threading import Lock

balance = 1000
lock = Lock()

def safe_withdraw(amount):
    global balance
    with lock:
        if balance >= amount:
            balance -= amount
            return True
        return False

# Результат: гарантированно 800

Решение 3: threading.Semaphore

import threading

balance = 1000
semaphore = threading.Semaphore(1)  # Макс. 1 поток одновременно

def withdraw(amount):
    global balance
    with semaphore:
        temp = balance
        temp -= amount
        balance = temp

Решение 4: Queue (рекомендуется для сложных случаев)

import threading
from queue import Queue

balance = 1000
command_queue = Queue()

def bank_worker():
    global balance
    while True:
        command = command_queue.get()
        if command["type"] == "withdraw":
            balance -= command["amount"]
        elif command["type"] == "deposit":
            balance += command["amount"]
        command_queue.task_done()

worker = threading.Thread(target=bank_worker, daemon=True)
worker.start()

# Отправляем команды в очередь
command_queue.put({"type": "withdraw", "amount": 100})
command_queue.put({"type": "withdraw", "amount": 100})

command_queue.join()
print(f"Balance: {balance}")  # 800

Типичные места Race Conditions

  1. Доступ к переменным — изменение из разных потоков
  2. Файловые операции — одновременное изменение файла
  3. Базы данных — одновременные запросы UPDATE/INSERT
  4. Кэширование — несоответствие данных в памяти и БД
  5. HTTP запросы — асинхронные запросы к API

Race Condition в базе данных

# ❌ Race condition: два пользователя переводят деньги одновременно
DEFECT: SELECT balance FROM accounts WHERE id = 1;  -- 1000
DEFECT: SELECT balance FROM accounts WHERE id = 1;  -- 1000
UPDATE accounts SET balance = 900 WHERE id = 1;  -- Поток 1
UPDATE accounts SET balance = 800 WHERE id = 1;  -- Поток 2 перезаписывает

# ✅ Решение: SELECT FOR UPDATE
BEGIN;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
-- Блокируем строку, другие потоки ждут
UPDATE accounts SET balance = 900 WHERE id = 1;
COMMIT;

Заключение

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

  1. Используй блокировки (Lock, Semaphore) для доступа к общим ресурсам
  2. Минимизируй критические секции — держи lock как можно меньше времени
  3. Избегай deadlock — всегда захватывай блокировки в одном порядке
  4. Используй Queue для асинхронной обработки в сложных сценариях
  5. Тестируй параллелизм — многократно запускай тесты в разных условиях