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

Ответ

Асинхронный 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().

Ответ 18+ 🔞

Да ты посмотри на эту дичь, блядь! Event loop в Python, сука, работает в одном потоке, а тут ты ему подсовываешь какую-нибудь ебучую задачу, которая процессор жрёт, как не в себя. Он, бедолага, на ней и встанет колом, весь твой асинхронный рай нахуй!

Чтобы такого пиздеца не было, эти вычислительные монстры надо выпинывать в отдельный процесс. Как? Да через run_in_executor() с 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 — это как отдельная камера пыток для твоей задачи. Он её запихивает в другой процесс, где та может ебашить все ядра процессора, а твой главный 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() — вообще красота, в рот меня чих-пых!

Вот и вся магия, ёпта. Не заставляй event loop страдать, выноси говнокод в отдельные процессы, и будет всем счастье.