Как в асинхронном коде Python выполнять CPU-bound задачи, не блокируя event loop?

«Как в асинхронном коде Python выполнять CPU-bound задачи, не блокируя event loop?» — вопрос из категории Асинхронность, который задают на 10% собеседований Python Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Асинхронный event loop в Python работает в одном потоке и блокируется длительными, ресурсоемкими (CPU-bound) вычислениями. Чтобы этого избежать, такие задачи необходимо выносить в отдельный процесс.

Основной способ — использовать run_in_executor() с concurrent.futures.ProcessPoolExecutor.

Пример:

import asyncio
from concurrent.futures import ProcessPoolExecutor
import time

def cpu_bound_operation(n):
    """Имитация тяжелой вычислительной задачи."""
    count = 0
    for i in range(n):
        count += i
    return count

async def main():
    loop = asyncio.get_running_loop()

    # Создаем пул процессов для выполнения CPU-bound задач
    with ProcessPoolExecutor() as pool:
        # Выносим блокирующую функцию в другой процесс,
        # не замораживая event loop.
        result = await loop.run_in_executor(
            pool, cpu_bound_operation, 10**7
        )
        print(f"Результат вычислений: {result}")

# Запускаем асинхронную программу
asyncio.run(main())

Ключевые моменты:

  • ProcessPoolExecutor запускает задачу в отдельном процессе, используя все доступные ядра CPU и не блокируя основной процесс с event loop.
  • loop.run_in_executor() "оборачивает" блокирующий вызов в awaitable объект, позволяя event loop продолжать обрабатывать другие задачи, пока вычисления выполняются в фоне.
  • Для I/O-bound задач (ожидание сети, диска) этот подход избыточен. Достаточно стандартных async/await и библиотек вроде aiohttp, так как event loop эффективно переключается между ними во время ожидания.
  • Альтернатива для потоков: Для блокирующих, но не ресурсоемких задач (например, вызов старой синхронной I/O-библиотеки), можно использовать ThreadPoolExecutor или, начиная с Python 3.9, более удобный asyncio.to_thread().