Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
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
- Запиши обратно в память
Вот что происходит в многопоточной программе:
Поток 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.