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

Что такое Data Race?

2.0 Middle🔥 91 комментариев
#DevOps и инфраструктура#Django

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

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

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

Data Race: конкурентный доступ к общим данным

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

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

import threading

counter = 0

def increment():
    global counter
    for _ in range(1000000):
        counter += 1

def decrement():
    global counter
    for _ in range(1000000):
        counter -= 1

# Создаём два потока
t1 = threading.Thread(target=increment)
t2 = threading.Thread(target=decrement)

t1.start()
t2.start()

t1.join()
t2.join()

print(f"Результат: {counter}")  # Должно быть 0, но часто бывает другое число!

Почему это происходит? Операция counter += 1 — это на самом деле три операции:

  1. Прочитай значение из памяти
  2. Добавь 1
  3. Запиши обратно в память

Вот что происходит в многопоточной программе:

Поток 1 (increment):           Поток 2 (decrement):
1. Читает counter = 0          
2. Добавляет 1                 
                                1. Читает counter = 0 (не видит изменение потока 1!)
                                2. Вычитает 1
                                3. Пишет counter = -1
3. Пишет counter = 1           

Результат: counter = 1 (вместо 0, потому что операция потока 2 была потеряна)

Типичные сценарии Data Race

1. Конфликт записи — несколько потоков изменяют одно значение

shared_list = []

def writer():
    for i in range(1000):
        shared_list.append(i)  # Data Race!

t1 = threading.Thread(target=writer)
t2 = threading.Thread(target=writer)
t1.start()
t2.start()
t1.join()
t2.join()

print(len(shared_list))  # Может быть < 2000 из-за потери данных

2. Проверка-и-действие (check-then-act)

balance = 1000

def withdraw(amount):
    global balance
    if balance >= amount:  # Проверка
        balance -= amount  # Действие — но между ними может вмешаться другой поток!

# Два потока пытаются снять по 600
t1 = threading.Thread(target=withdraw, args=(600,))
t2 = threading.Thread(target=withdraw, args=(600,))
t1.start()
t2.start()
t1.join()
t2.join()

print(f"Баланс: {balance}")  # Может быть -200 вместо 400!

Как решить проблему Data Race

1. Использовать Lock (мьютекс)

import threading

counter = 0
lock = threading.Lock()

def increment():
    global counter
    for _ in range(1000000):
        with lock:  # Гарантирует, что только один поток может входить
            counter += 1

def decrement():
    global counter
    for _ in range(1000000):
        with lock:
            counter -= 1

t1 = threading.Thread(target=increment)
t2 = threading.Thread(target=decrement)
t1.start()
t2.start()
t1.join()
t2.join()

print(f"Результат: {counter}")  # Теперь точно 0

2. Использовать thread-safe структуры данных

from queue import Queue
from threading import Thread

# Queue уже thread-safe
queue = Queue()

def producer():
    for i in range(100):
        queue.put(i)  # Безопасно

def consumer():
    while True:
        try:
            item = queue.get(timeout=1)  # Безопасно
        except:
            break

t1 = Thread(target=producer)
t2 = Thread(target=consumer)
t1.start()
t2.start()

3. Использовать asyncio вместо многопоточности

import asyncio

counter = 0

async def increment():
    global counter
    for _ in range(1000000):
        counter += 1
        await asyncio.sleep(0)  # Явная точка переключения контекста

async def main():
    await asyncio.gather(
        increment(),
        increment()
    )

asyncio.run(main())

4. Использовать multiprocessing вместо threading

from multiprocessing import Process, Value
from ctypes import c_int

def increment(counter):
    for _ in range(1000000):
        counter.value += 1

if __name__ == '__main__':
    counter = Value(c_int, 0)
    p1 = Process(target=increment, args=(counter,))
    p2 = Process(target=increment, args=(counter,))
    p1.start()
    p2.start()
    p1.join()
    p2.join()
    print(counter.value)  # Каждый процесс имеет свой GIL

Почему это сложно

Data Races трудно отловить:

  • Код может работать правильно в 99.9% случаев
  • Проблема проявляется случайно в зависимости от планировщика ОС
  • Трудно воспроизвести баг
  • Отладчик может скрывать проблему (добавляет задержки)

Инструменты для поиска Data Races

# ThreadSanitizer (для C/C++, но есть расширения для Python)
# Helgrind (часть Valgrind)
# sys.settrace() для отладки многопоточного кода

import sys
import threading

def trace_calls(frame, event, arg):
    if event == 'return':
        print(f"Поток {threading.current_thread().name}: {frame.f_code.co_name}")
    return trace_calls

sys.settrace(trace_calls)

Вывод

Data Race — это серьёзная проблема многопоточного программирования. Правило простое: если несколько потоков обращаются к одной переменной, и хотя бы один её изменяет — нужна синхронизация через Lock, Semaphore, Event или другие примитивы. В Python часто проще использовать asyncio или multiprocessing вместо threading.