← Назад к вопросам
Что такое состояние гонки (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
- Доступ к переменным — изменение из разных потоков
- Файловые операции — одновременное изменение файла
- Базы данных — одновременные запросы UPDATE/INSERT
- Кэширование — несоответствие данных в памяти и БД
- 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;
Заключение
Состояния гонки — это опасные ошибки, которые трудно воспроизвести и отладить. Ключевые моменты:
- Используй блокировки (Lock, Semaphore) для доступа к общим ресурсам
- Минимизируй критические секции — держи lock как можно меньше времени
- Избегай deadlock — всегда захватывай блокировки в одном порядке
- Используй Queue для асинхронной обработки в сложных сценариях
- Тестируй параллелизм — многократно запускай тесты в разных условиях