Приведён фрагмент Python-кода для многопоточности: import threading; counter=0; def inc(): global counter; for _ in range(100000): counter+=1; threads=[threading.Thread(target=inc) for _ in range(5)]; for t in threads: t.start(); for t in threads: t.join(); — объясните, почему итоговый counter может быть меньше ожидаемого, и предложите корректные решения с объяснением производительности

16 Мар в 09:14
15 +1
0
Ответы
1
Почему результат может быть меньше ожидаемого
- Операция `counter += 1` не атомарна: она разбивается на несколько байт-кодов (чтение, сложение, запись). Потоки могут прерывать друг друга между этими шагами, из‑за чего одно обновление перезапишет другое (race condition). В CPython есть GIL, но он не делает сложные составные операции атомарными — переключение потоков может произойти между байт-кодами.
Ожидаемый итог: 5×100000=500000 \;5\times100000=500000\;5×100000=500000. На практике получите меньше из‑за потерянных обновлений.
Корректные решения и их производительность
1) Защитить критическую секцию lock'ом (правильно и просто)
- Код:
lock = threading.Lock()
def inc():
global counter
for _ in range(100000):
with lock:
counter += 1
- Пояснение: устраняет гонки. Недостаток — сильная конкуренция: каждый инкремент сериализуется, поэтому для чисто CPU‑Bound задач фактически теряется параллелизм и скорость близка к однопоточному (GIL + блокировки).
2) Локальные счётчики + итоговое объединение (рекомендуемое при большом числе мелких операций)
- Идея: каждая нить считает в локальной переменной, затем один раз добавляет результат в общий счётчик под блокировкой.
- Код:
def inc():
local = 0
for _ in range(100000):
local += 1
with lock:
global counter
counter += local
- Пояснение: совсем мало блокировок (по одному на поток), значительно лучше производительность при частых коротких операциях.
3) Использовать multiprocessing для CPU‑Bound задач (полезно, если нужно реально загрузить несколько ядер)
- Идея: запустить процессы, делить работу между процессами и суммировать результаты; можно использовать multiprocessing.Value или просто возвращать значения из Pool.
- Плюсы: минует GIL, позволяет параллелить CPU‑интенсивную работу.
- Минусы: дороже по памяти/IPC и по времени запуска процессов; выгодно при достаточно крупной работе.
4) Атомарные операции из C-расширения или сторонних библиотек
- Можно использовать реализованную на C атомарную переменную (third‑party), или написать C‑расширение/ctypes.
- Пояснение: даёт атомарный инкремент без Python‑уровневых блокировок, очень быстро, но требует внешнего кода.
Краткие рекомендации
- Для вашего примера оптимально: либо локальные счётчики + объединение (простое и быстрое), либо multiprocessing если задача CPU‑интенсивна и большая (чтобы использовать несколько ядер). Lock на каждый инкремент корректен, но будет медленным из‑за сериализации.
16 Мар в 10:02
Не можешь разобраться в этой теме?
Обратись за помощью к экспертам
Гарантированные бесплатные доработки в течение 1 года
Быстрое выполнение от 2 часов
Проверка работы на плагиат
Поможем написать учебную работу
Прямой эфир