> Как параллелить выполнение функции в Python (Python)
Уровень: senior · Роль: backend · Категория: Технические вопросы
Компании: Сбер
Стек: Python
> Пример ответа
Короткий ответ
Параллельное выполнение функций в Python зависит от типа задачи: для I/O-bound используйте asyncio или потоки (concurrent.futures.ThreadPoolExecutor), для CPU-bound - процессы (ProcessPoolExecutor) или multiprocessing. Из-за GIL потоки не дают прироста для CPU-нагрузки. Выбор инструмента определяется характером работы: асинхронность - для сетевых операций, потоки - для блокирующего I/O, процессы - для вычислений.
Подробное объяснение
В Python параллелизм реализуется тремя основными механизмами:
-
Потоки (threads) - легковесны, но ограничены GIL (Global Interpreter Lock). GIL позволяет выполнять только один байткод за раз, поэтому для CPU-bound задач потоки бесполезны. Для I/O-bound - эффективны, так как GIL освобождается во время системных вызовов.
-
Процессы (processes) - каждый процесс имеет собственную память и свой GIL. Полный параллелизм на многоядерных системах. Минусы: overhead на создание, межпроцессное взаимодействие (pickle, shared memory).
-
Асинхронность (asyncio) - кооперативная многозадачность в одном потоке. Идеально для сетевых операций и большого числа конкурентных задач. Требует асинхронных библиотек (aiohttp, asyncpg).
Ключевой trade-off: потоки - простота и привычность, процессы - максимальная производительность для CPU, asyncio - масштабируемость для I/O.
На практике
Для типичного backend-сценария (обработка запросов, обращение к внешним API, работа с БД) чаще всего используется asyncio или ThreadPoolExecutor. Если функция блокирующая (например, синхронный HTTP-клиент), её оборачивают в run_in_executor или используют asyncio.to_thread.
Для CPU-интенсивных задач (парсинг, обработка изображений, ML-инференс) - ProcessPoolExecutor с concurrent.futures. Важно помнить про передачу данных между процессами: она сериализуется через pickle, что может быть дорого.
Пример кода
PYTHONimport asyncioimport timefrom concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor# I/O-bound: асинхронноasync def fetch_url(url: str) -> str:await asyncio.sleep(0.1) # имитация сетевого вызоваreturn f"data from {url}"async def run_async():urls = [f"http://example.com/{i}" for i in range(10)]results = await asyncio.gather(*(fetch_url(u) for u in urls))return results# I/O-bound: потокиdef blocking_io(item: int) -> int:time.sleep(0.1)return item * 2def run_threads():with ThreadPoolExecutor(max_workers=4) as executor:results = list(executor.map(blocking_io, range(10)))return results# CPU-bound: процессыdef cpu_intensive(n: int) -> int:return sum(i * i for i in range(n))def run_processes():with ProcessPoolExecutor(max_workers=4) as executor:results = list(executor.map(cpu_intensive, [10_000_000] * 4))return results# Комбинированный подход: асинхронность + потоки для блокирующих вызововasync def hybrid():loop = asyncio.get_running_loop()result = await loop.run_in_executor(None, blocking_io, 42)return result
Как отвечать на собеседовании
Начните с классификации задач: I/O-bound vs CPU-bound. Затем объясните GIL и его влияние. Покажите понимание trade-off: потоки - просто, но ограничены; процессы - мощно, но дорого; asyncio - масштабируемо, но требует асинхронного кода. Упомяните concurrent.futures как высокоуровневый API. Приведите пример выбора для конкретного сценария. Если спросят про multiprocessing - сравните с ProcessPoolExecutor (первый - низкоуровневый, второй - удобнее).
Что проверяет интервьюер
- Понимание GIL и его последствий.
- Умение классифицировать задачи по типу нагрузки.
- Знание конкретных инструментов и их ограничений.
- Понимание overhead'ов (создание процессов, сериализация данных).
- Способность выбрать правильный механизм под задачу, а не использовать один инструмент для всего.
Типичные ошибки
- Использование потоков для CPU-bound задач - ожидание ускорения, которого не будет из-за GIL.
- Игнорирование
asyncioдля I/O, выбор потоков без необходимости. - Создание процесса на каждую задачу без пула - огромный overhead.
- Передача больших данных между процессами через pickle - узкое место.
- Забывают про
asyncio.to_threadдля блокирующих вызовов внутри асинхронного кода. - Не учитывают, что
ProcessPoolExecutorтребует, чтобы функция была pickle-able (например, не может быть lambda или методом экземпляра).
> Похожие задачи по backend
Как посчитать возраст в годах между текущей датой и датой рождения в Python
Как вывести в одном датафрейме результаты по студентам с баллами по математике и биологии ниже 80
Что должно присутствовать у объекта, чтобы он был итератором в Python
Какие типы данных существуют в Python
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью