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

Почему возникает состояние гонки между потоками?

1.0 Junior🔥 131 комментариев
#Асинхронность и многопоточность

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

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

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

Состояние гонки (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()  # Безопасно

Заключение

Состояние гонки возникает потому что:

  1. ОС прерывает потоки в неопредсказуемые моменты
  2. Операции чтения-модификации-записи не атомарны
  3. Несколько потоков обращаются к одному ресурсу без синхронизации
  4. Кэши процессора не синхронизируются мгновенно

Решение: использовать примитивы синхронизации (Lock, Semaphore, Queue) для защиты критических секций кода.