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

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

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

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

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

Именно так работает классический синхронный код. Программа выполняет инструкции строго последовательно. Если одна из задач требует ожидания (например, скачивание файла из интернета или запрос к базе данных), весь процесс останавливается. Процессор простаивает, хотя мог бы выполнять другую полезную работу.

В программировании такие задачи называются I/O-bound (ограниченные вводом-выводом). Для их эффективного решения в Python используется асинхронное программирование.

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

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

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

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

Синхронный vs Асинхронный подход

| Характеристика | Синхронный код | Асинхронный код (asyncio) | | :--- | :--- | :--- | | Порядок выполнения | Строго последовательный | Конкурентный (переключение между задачами) | | Ожидание I/O | Блокирует всю программу | Программа продолжает выполнять другие задачи | | Потребление ресурсов | Высокое (если использовать много потоков) | Низкое (работает в одном потоке) | | Сложность кода | Низкая (просто читать сверху вниз) | Средняя (требует понимания новых ключевых слов) |

Сердце асинхронности: Event Loop

Главный механизм, который управляет всей этой магией в Python — это Event Loop (цикл событий).

Event Loop — это бесконечный цикл, который работает в одном потоке. Он постоянно проверяет очередь задач. Если задача готова к выполнению, он запускает её. Если задача доходит до момента, где ей нужно подождать (например, ответа от сервера), она сообщает об этом Event Loop. Цикл событий откладывает эту задачу и берет из очереди следующую.

!Архитектура Event Loop и корутин

Упрощенно работу Event Loop можно представить в виде ASCII-схемы:

Корутины и синтаксис async/await

Чтобы Event Loop понимал, какие функции могут приостанавливать свою работу, в Python 3.5+ ввели специальные ключевые слова: async и await.

Обычная функция определяется через def. Асинхронная функция определяется через async def. Вызов такой функции не выполняет её код сразу, а возвращает специальный объект — корутину (coroutine).

Рассмотрим базовый пример:

Подробный разбор кода

  • import asyncio — импортируем стандартную библиотеку для асинхронного программирования.
  • async def say_hello(): — создаем корутину. Внутри неё мы можем использовать ключевое слово await.
  • await asyncio.sleep(1) — это критически важный момент. Функция asyncio.sleep(1) имитирует долгую операцию ввода-вывода (например, сетевой запрос). Ключевое слово await говорит Python: «Я буду ждать здесь 1 секунду. Пока я жду, Event Loop может заняться другими делами».
  • asyncio.run(say_hello()) — эта команда создает Event Loop, запускает переданную корутину до полного завершения и затем закрывает цикл событий. Это стандартная точка входа в любую асинхронную программу.
  • Магия конкурентности: запуск нескольких задач

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

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

    Докажем это на практике:

    Вывод программы будет следующим:

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

    Обратите внимание: хотя сумма времени всех загрузок составляет 6 секунд (2 + 3 + 1), программа выполнилась ровно за 3 секунды. Event Loop запустил первую задачу, дошел до await, переключился на вторую, затем на третью. Все три задачи «спали» одновременно.

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

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

    Ошибка 1: Забытый await

    Если вы вызовете корутину без await, она не выполнится.

    Вызов do_work() просто создает объект корутины в памяти, но не планирует её выполнение в Event Loop. Правильно: await do_work().

    Ошибка 2: Блокирующие вызовы внутри async

    Это самая опасная ошибка. Использование синхронных функций, которые блокируют поток (например, time.sleep() или requests.get()), внутри асинхронного кода ломает всю концепцию.

    Поскольку Event Loop работает в одном потоке, time.sleep(5) усыпляет весь поток. Никакие другие асинхронные задачи в этот момент выполняться не смогут. Всегда используйте асинхронные аналоги: asyncio.sleep() вместо time.sleep(), aiohttp или httpx вместо requests.

    Практическое применение

    Чтобы закрепить материал, давайте напишем небольшую программу.

    Задача 1: Написать функцию Создайте асинхронную функцию fetch_data(id, delay), которая принимает ID пользователя и задержку. Функция должна выводить сообщение о начале запроса, ждать указанное время с помощью asyncio.sleep(), а затем возвращать строку вида "Данные пользователя {id}".

    Задача 2: Реализовать конкурентный запуск Напишите функцию main(), которая с помощью asyncio.gather() запрашивает данные для пользователей с ID 1, 2 и 3 с задержками 2, 1 и 3 секунды соответственно. Выведите результаты на экран.

    Попробуйте написать код самостоятельно, прежде чем смотреть на решение ниже.

    В этом уроке мы заложили фундамент. Мы узнали, что асинхронность позволяет эффективно утилизировать время простоя при I/O операциях, познакомились с Event Loop и научились писать базовые корутины. В следующей статье мы глубже погрузимся в механику работы Event Loop и узнаем, как именно он планирует задачи под капотом.