Вопрос проверяет понимание ограничений асинхронного программирования в Python и того, как CPU-bound задачи блокируют цикл событий, снижая производительность.
Асинхронное программирование в Python с помощью модуля asyncio отлично подходит для I/O-bound операций, таких как запросы к сети или чтение файлов, где корутины могут "отдавать" управление, ожидая завершения операции. Однако для задач, интенсивно использующих процессор (CPU-bound), эта модель ломается.
Цикл событий asyncio работает в одном потоке. Корутина выполняется до тех пор, пока не встретит операцию await, которая сигнализирует о возможной блокировке (например, ожидании ответа от сети). В этот момент управление возвращается в event loop, который может запустить другую корутину. CPU-bound задача, по своей природе, не содержит таких точек await — она просто выполняет вычисления, не отдавая управление. Таким образом, event loop "застывает" до завершения этих вычислений.
import asyncio
import time
# CPU-bound функция (имитация тяжелых вычислений)
def cpu_intensive_task():
result = 0
for i in range(10**7):
result += i
return result
async def cpu_bound_coroutine():
print("Начало CPU-bound задачи...")
# Прямой вызов блокирующей функции — ОШИБКА!
result = cpu_intensive_task()
print(f"CPU-bound задача завершена: {result}")
async def another_coroutine():
for i in range(5):
print(f"Другая корутина работает: {i}")
await asyncio.sleep(0.1) # Точка await, где управление возвращается
async def main():
# Запускаем обе корутины "одновременно"
await asyncio.gather(
cpu_bound_coroutine(),
another_coroutine()
)
# Запуск
asyncio.run(main())В этом примере another_coroutine не будет выполняться, пока не завершится cpu_intensive_task, потому что последняя не содержит await и блокирует поток.
Для выполнения CPU-bound задач в асинхронном приложении нужно вынести их в отдельные потоки или процессы, чтобы не блокировать основной поток event loop. Модуль asyncio предоставляет для этого:
loop.run_in_executor() — запуск блокирующего кода в пуле потоков или процессов.concurrent.futures для создания собственного исполнителя (executor).Исправленный вариант с использованием исполнителя:
import asyncio
from concurrent.futures import ProcessPoolExecutor
def cpu_intensive_task():
result = 0
for i in range(10**7):
result += i
return result
async def cpu_bound_coroutine():
print("Начало CPU-bound задачи...")
loop = asyncio.get_running_loop()
# Запускаем в отдельном процессе
result = await loop.run_in_executor(None, cpu_intensive_task)
print(f"CPU-bound задача завершена: {result}")
async def another_coroutine():
for i in range(5):
print(f"Другая корутина работает: {i}")
await asyncio.sleep(0.1)
async def main():
await asyncio.gather(
cpu_bound_coroutine(),
another_coroutine()
)
asyncio.run(main())Теперь cpu_intensive_task выполняется в отдельном потоке/процессе, и event loop может переключаться на другие корутины.
Вывод: CPU-bound задачи в корутинах блокируют цикл событий, уничтожая преимущества асинхронности. Для их выполнения используйте run_in_executor или выносите в отдельные процессы, особенно в высоконагруженных I/O-приложениях, таких как веб-серверы на aiohttp или FastAPI.
Уровень
Рейтинг:
4
Сложность:
6
Навыки
Python
Node.js
Ключевые слова
Подпишись на Python Developer в телеграм