Почему возникает состояние гонки между потоками?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Состояние гонки (Race Condition) между потоками
Состояние гонки — это критическая проблема в многопоточном программировании, когда два или более потока обращаются к общему ресурсу одновременно и результат зависит от порядка их выполнения.
1. Фундаментальная причина: Нелинейное выполнение
ОС управляет потоками и может в любой момент прервать поток (context switch), чтобы дать процессорное время другому потоку.
# Простой пример: счётчик, который увеличивают два потока
counter = 0
def increment():
global counter
counter += 1 # Это НЕ атомарная операция!
# Поток 1: increment()
# Поток 2: increment()
# Ожидаем: counter = 2
# Может быть: counter = 1 (гонка!)
Почему counter += 1 опасна? Это на самом деле три операции:
; Сборка для counter += 1
MOV eax, [counter] ; 1. Прочитать текущее значение из памяти
ADD eax, 1 ; 2. Добавить 1 к регистру
MOV [counter], eax ; 3. Записать результат обратно в память
Сценарий гонки:
Время | Поток 1 | Поток 2 | counter
-------|----------------------|----------------------|---------
1 | читает counter (0) | - | 0
2 | - | читает counter (0) | 0
3 | добавляет 1 → 1 | - | 0
4 | - | добавляет 1 → 1 | 0
5 | пишет 1 в counter | - | 1
6 | - | пишет 1 в counter | 1
Оба потока увидели counter=0, каждый прибавил 1, но результат — 1, а не 2!
2. Потому что память и регистры не синхронизированы
Каждый поток имеет свой кэш процессора, и изменения могут не видны другим потокам мгновенно.
import threading
balance = 1000
def transfer_money():
"""Эта функция попытается совершить некорректный расчёт"""
global balance
temp = balance # Поток 1: temp = 1000
# <- ЗДЕСЬ может произойти context switch
balance = temp - 100 # Поток 1: balance = 900
thread1 = threading.Thread(target=transfer_money)
thread2 = threading.Thread(target=transfer_money)
# Если оба потока прочитают balance=1000, то итоговый результат = 900
# Вместо ожидаемого 800
3. Множественные потоки, один ресурс
import threading
import time
class BankAccount:
def __init__(self, balance):
self.balance = balance
def withdraw(self, amount):
"""Опасная функция без синхронизации"""
if self.balance >= amount:
# Задержка имитирует долгую операцию
time.sleep(0.0001) # <- Поток может быть прерван здесь
self.balance -= amount
print(f"Снято {amount}, осталось {self.balance}")
else:
print("Недостаточно средств")
account = BankAccount(1000)
# Оба потока пытаются снять 600 из 1000
t1 = threading.Thread(target=account.withdraw, args=(600,))
t2 = threading.Thread(target=account.withdraw, args=(600,))
t1.start()
t2.start()
t1.join()
t2.join()
print(f"Финальный баланс: {account.balance}") # Может быть -200 вместо 400!
4. Проблема: Несовместимость операций
Операция считается атомарной (atomic), если она не может быть прервана. В Python операции НЕ атомарны:
# ❌ НЕ атомарна
item = list[index]
# ❌ НЕ атомарна
dict[key] = value
# ❌ НЕ атомарна
counter += 1
# ✅ Атомарна (благодаря GIL, но не всегда надёжна)
queue.put(item)
5. Непредсказуемость гонки
Гонка проявляется недетерминированно, что делает её очень коварной.
import threading
results = []
def race_condition():
for _ in range(100000):
x = 0
x += 1 # Гонка
if x != 1:
results.append("ГОНКА ОБНАРУЖЕНА!") # Редко, случайно
threads = [threading.Thread(target=race_condition) for _ in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
if results:
print(f"Обнаружено {len(results)} случаев гонки")
else:
print("Гонка не обнаружена (но она есть!)")
Почему сложно обнаружить: Гонка может не проявиться при первом запуске, зависит от планировщика ОС.
6. Классические примеры гонки
Пример 1: Счётчик посещений
visits = 0 # Общая переменная
def user_visit():
global visits
visits += 1 # ГОНКА!
# 1000 потоков одновременно
threads = [threading.Thread(target=user_visit) for _ in range(1000)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"Посещений: {visits}") # Может быть < 1000!
Пример 2: Чтение-модификация-запись одновременно
class UserProfile:
def __init__(self):
self.followers = 0
def add_follower(self):
self.followers += 1 # ГОНКА!
def remove_follower(self):
self.followers -= 1 # ГОНКА!
profile = UserProfile()
# Один поток добавляет подписчиков, другой удаляет
t1 = threading.Thread(
target=lambda: [profile.add_follower() for _ in range(1000)]
)
t2 = threading.Thread(
target=lambda: [profile.remove_follower() for _ in range(500)]
)
t1.start()
t2.start()
t1.join()
t2.join()
print(f"Followers: {profile.followers}") # Может быть непредсказуемым
7. Решения: Синхронизация потоков
Вариант 1: Lock (Mutex)
import threading
class ThreadSafeBankAccount:
def __init__(self, balance):
self.balance = balance
self.lock = threading.Lock() # Мьютекс
def withdraw(self, amount):
with self.lock: # Критическая секция
if self.balance >= amount:
self.balance -= amount
print(f"Снято {amount}")
else:
print("Недостаточно средств")
Вариант 2: Semaphore
semaphore = threading.Semaphore(3) # Максимум 3 потока одновременно
with semaphore:
# Критическая операция
pass
Вариант 3: RLock (Recursive Lock)
lock = threading.RLock() # Один поток может заблокировать несколько раз
with lock:
with lock: # Один и тот же поток может войти дважды
pass
Вариант 4: Queue (безопасная очередь)
from queue import Queue
data_queue = Queue() # Thread-safe
def producer():
data_queue.put("data") # Безопасно
def consumer():
item = data_queue.get() # Безопасно
Заключение
Состояние гонки возникает потому что:
- ОС прерывает потоки в неопредсказуемые моменты
- Операции чтения-модификации-записи не атомарны
- Несколько потоков обращаются к одному ресурсу без синхронизации
- Кэши процессора не синхронизируются мгновенно
Решение: использовать примитивы синхронизации (Lock, Semaphore, Queue) для защиты критических секций кода.