Асинхронное программирование на Python

Практический курс по асинхронному программированию на Python 3.10+ для разработчиков. Вы изучите работу Event Loop, корутины, управление задачами, асинхронный I/O и создадите высоконагруженный сервис сбора данных.

1. Введение в асинхронность: Event Loop, корутины и синтаксис async/await

Введение в асинхронность: Event Loop, корутины и синтаксис async/await

Представьте кухню ресторана. Повар получает заказ на приготовление сложного блюда: нужно сварить бульон, пожарить мясо и нарезать салат.

При синхронном подходе повар ставит кастрюлю с водой на плиту и стоит рядом, глядя на нее 30 минут, пока вода не закипит. Только после этого он начинает жарить мясо, а закончив с мясом — режет салат. Это крайне неэффективно.

При асинхронном подходе повар ставит воду на плиту, заводит таймер и сразу же переключается на нарезку салата. Когда таймер звенит, повар на секунду отвлекается, чтобы закинуть ингредиенты в кипящую воду, и возвращается к мясу. Повар один, но задачи выполняются параллельно благодаря грамотному переключению между ними во время ожидания.

В программировании ожидание закипания воды — это I/O-операции (Input/Output). К ним относятся сетевые запросы, чтение файлов или обращение к базе данных. Процессор (повар) работает в миллионы раз быстрее, чем сеть или жесткий диск. Асинхронное программирование позволяет процессору не простаивать в ожидании ответа, а выполнять другой полезный код.

!Сравнение синхронного и асинхронного выполнения

Кооперативная многозадачность

Асинхронность в Python базируется на концепции кооперативной многозадачности (cooperative multitasking).

В отличие от потоков (threads), которыми управляет операционная система (вытесняющая многозадачность), в асинхронном программировании задачи сами решают, когда им приостановиться и уступить место другим. Они «сотрудничают» друг с другом.

> Кооперативная многозадачность требует явного указания в коде мест, где функция готова отдать управление. Если одна задача решит выполнять сложные математические вычисления и не отдаст управление, все остальные задачи будут заблокированы.

Для расчета выигрыша во времени можно использовать простую математику. Общее время выполнения синхронной программы выражается формулой:

Где — общее время, — количество задач, а — время выполнения каждой отдельной задачи. Если у нас 100 сетевых запросов по 1 секунде каждый, общее время составит 100 секунд.

В идеальной асинхронной среде общее время стремится к времени выполнения самой долгой задачи:

Где — небольшие накладные расходы на переключение контекста. Те же 100 запросов выполнятся примерно за 1.1 секунды.

Три кита асинхронности в Python

Современный асинхронный Python (начиная с версии 3.5 и особенно в 3.10+) опирается на три фундаментальных понятия: корутины, синтаксис async/await и Event Loop.

1. Корутины (Coroutines)

Корутина (сопрограмма) — это специальная функция, выполнение которой можно приостановить, а затем возобновить с того же места.

Обычная функция (подпрограмма) всегда выполняется от начала до конца и возвращает результат через return. Корутина же может сказать: «Я жду данные из сети, поставьте меня на паузу».

Чтобы превратить обычную функцию в корутину, перед def добавляется ключевое слово async.

Обратите внимание: вызов my_coroutine() не выполняет код внутри функции. Он лишь создает объект корутины. Чтобы код выполнился, корутину нужно запустить внутри цикла событий.

2. Синтаксис async / await

Ключевое слово await используется только внутри async-функций. Оно означает: «Приостанови выполнение этой корутины до тех пор, пока ожидаемая операция не завершится. Тем временем процессор может заняться другими делами».

Если бы мы использовали синхронный time.sleep(1) три раза подряд, программа работала бы 3 секунды. Благодаря await asyncio.sleep(1), программа работает всего 1 секунду, так как все три «загрузки» происходят параллельно.

3. Event Loop (Цикл событий)

Event Loop — это сердце асинхронного приложения. Это бесконечный цикл, который управляет выполнением всех корутин.

Его логику можно описать тремя шагами:

  • Взять задачу из очереди готовых к выполнению.
  • Выполнять её до тех пор, пока она не завершится или не встретит ключевое слово await.
  • Если задача встретила await, отложить её в список ожидающих и взять следующую готовую задачу.
  • !Визуализация работы Event Loop

    Когда мы вызываем asyncio.run(main()), под капотом создается новый Event Loop, в него помещается корутина main(), цикл запускается и работает до тех пор, пока main() не завершится. После этого цикл уничтожается.

    Сравнение подходов к конкурентности

    В Python есть три основных способа выполнять задачи параллельно или конкурентно. Важно понимать, когда какой инструмент применять.

    | Характеристика | Многопроцессорность (multiprocessing) | Многопоточность (threading) | Асинхронность (asyncio) | | :--- | :--- | :--- | :--- | | Сущность | Процессы ОС | Потоки ОС | Корутины (внутри Python) | | Память | Высокое потребление (изолированная память) | Среднее потребление (общая память) | Минимальное потребление (килобайты на корутину) | | Переключение контекста | Очень медленное | Среднее | Очень быстрое | | Идеально подходит для | CPU-bound задач (математика, обработка видео) | I/O-bound задач (если библиотеки не поддерживают async) | Массовых I/O-bound задач (тысячи сетевых запросов) | | Влияние GIL | Обходит GIL (каждый процесс имеет свой GIL) | Ограничено GIL (выполняется только один поток Python-кода одновременно) | Работает в одном потоке, GIL не мешает I/O-операциям |

    Типичные ошибки новичков

    При переходе на асинхронное программирование разработчики часто совершают концептуальные ошибки.

    Блокировка Event Loop синхронным кодом. Если внутри async функции вызвать синхронную блокирующую функцию (например, requests.get() или time.sleep()), она заблокирует весь поток. Event Loop остановится, и ни одна другая корутина не сможет выполняться.

    Забытый await. Если вызвать корутину без await, она не выполнится. Python просто создаст объект корутины и пойдет дальше, а вы получите предупреждение RuntimeWarning: coroutine '...' was never awaited.

    Асинхронное программирование требует изменения мышления. Вы больше не пишете код, который выполняется строго линейно. Вы описываете набор задач и указываете моменты, когда эти задачи могут подождать, позволяя системе работать с максимальной эффективностью.