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 и узнаем, как именно он планирует задачи под капотом.