Асинхронность в aiogram: asyncio для бота без страха
Разбираем на живом коде, зачем aiogram асинхронный: async и await, asyncio.sleep против time.sleep и gather для параллельных чатов.
Редакция Питоники
Урок, ради которого aiogram и выбрали вместо синхронных библиотек. Ваш бот общается не с одним Аней: утром сотня человек шлёт сообщения почти одновременно. Бот на обычном, синхронном коде отвечает им по очереди — и где-то в конце очереди люди ждут минуты. aiogram построен на asyncio, поэтому обслуживает всех сразу. В этом уроке вы своими руками увидите, почему бот замирает, как устроены async и await, как запустить задачи параллельно и что такое фоновые задачи.
Хорошая новость: asyncio — часть стандартного Python, и в песочнице этой страницы он работает по-настоящему. Каждый блок ниже — живой код с настоящими корутинами: запускайте, ломайте, меняйте имена. Порядок вывода в примерах скажет об асинхронности больше, чем любые диаграммы.
Почему бот замирает: синхронная очередь
Сначала боль. Обычная функция занимает всё внимание программы от первого оператора до последнего. Если внутри неё «долгая работа» — запрос к API, чтение файла — программа стоит и ждёт. Посмотрите на синхронного бота, у которого пришло два сообщения. Код ниже содержит настоящий time.sleep: при запуске вы почувствуете паузы сами.
import time
# Синхронный "бот": сообщения обрабатываются строго по очереди.
# Запустите - и почувствуйте паузы: пока Аня обслуживается,
# Борис просто ждёт своей очереди.
def handle(user):
print(user + ": запрос получен")
time.sleep(0.4) # "долгая" работа, весь бот замирает
print(user + ": ответ отправлен")
handle("Аня")
handle("Борис")
Аня: запрос получен Аня: ответ отправлен Борис: запрос получен Борис: ответ отправлен
Вывод скучный и в этом весь ужас: Аня полностью обслужена, только потом тронули Бориса. С time.sleep(0.4) это полсекунды, а с запросом к медленному API — три секунды на каждого человека. Сотня пользователей — последние ждут пять минут. Расширять команду до тысячи ядер бессмысленно: программа-то одна и она занята ожиданием.
А теперь главное наблюдение. Бот почти ничего не считает — он ждёт: ответ от серверов Telegram, запись на диск, ответ веб-сервиса. Пока идёт ожидание, процессор свободен. Значит, ждать можно сразу для многих: пока Аня ждёт ответ Telegram, Бориса уже можно обслуживать. Для этого программе нужно уметь ставить функцию на паузу и переключаться — и именно это делают async и await.
Что такое async, await и корутина
Слово async перед def создаёт не обычную функцию, а корутину — функцию с правом на паузу. Внутри корутины можно написать await: «здесь я жду, отдай управление другим». Вызывающая сторона обязана запустить корутину — сама по себе она не выполняется.
import asyncio
async def greet(name):
print("Готовлю приветствие для", name)
await asyncio.sleep(0) # вежливая пауза: отдаём управление
print("Привет,", name)
# без asyncio.run корутина - просто объект, она не выполняется
asyncio.run(greet("Аня"))
Готовлю приветствие для Аня Привет, Аня
Три вещи, которые здесь произошли. async def greet — объявление корутины. greet("Аня") — не выполнение, а создание корутинного объекта, как list() создаёт список, не печатая его. asyncio.run(...) — запуск цикла событий (event loop): диспетчера, который выполняет корутины и переключает их в точках await. await asyncio.sleep(0) — пауза ноль секунд, полезная ровно одним: она показывает циклу «пока я, можешь запустить кого-то ещё».
Чем asyncio.sleep отличается от time.sleep
Теперь два сообщения одновременно. Запустим обе корутины параллельно через gather и понаблюдаем порядок строк — он не совпадёт с синхронной версией:
import asyncio
async def chat(user):
print(user + ": запрос получен")
await asyncio.sleep(0.2) # "запрос к Telegram": ждём, но не блокируем
print(user + ": ответ отправлен")
async def main():
# два сообщения пришли одновременно - обслуживаем оба
await asyncio.gather(chat("Аня"), chat("Борис"))
asyncio.run(main())
Аня: запрос получен Борис: запрос получен Аня: ответ отправлен Борис: ответ отправлен
Сравните с первым блоком урока: строки те же, порядок другой. Обе корутины успели напечатать «запрос получен» до того, как закончилось ожидание первой — то есть Аня и Борис обслуживаются одновременно. Никакой многопоточности здесь нет: один поток, один цикл событий, который просто умно переключает корутины в точках await. Разница между двумя sleep'ами ровно в этом:
| time.sleep | asyncio.sleep | |
|---|---|---|
| Что делает | останавливает весь поток | ставит на паузу только свою корутину |
| Цикл событий | заблокирован, бот молчит для всех | живёт, обслуживает других |
| Где можно | в обычном коде | только внутри async def |
| В хендлере aiogram | никогда | всегда |
time.sleep останавливает весь цикл событий, а asyncio.sleep ставит на паузу только одну корутину. Одна буква в начале строки — а на другом конце разница между ботом и статуей.
gather: параллельные задачи и порядок результатов
asyncio.gather — главный способ запустить несколько корутин разом: передаёте их аргументами, он возвращает список результатов. У gather есть приятная неожиданность: корутины финишируют в порядке своих задержек, но список результатов идёт в порядке аргументов. Проверим на трёх задачах с разными паузами:
import asyncio
async def work(name, seconds):
await asyncio.sleep(seconds)
print("закончила:", name)
return name + " готова"
async def main():
results = await asyncio.gather(
work("медленная", 0.3),
work("быстрая", 0.1),
work("средняя", 0.2),
)
# порядок результатов - как у аргументов, а не как у финиша
print("gather вернул:", results)
asyncio.run(main())
закончила: быстрая закончила: средняя закончила: медленная gather вернул: ['медленная готова', 'быстрая готова', 'средняя готова']
Финишный порядок диктуют паузы: быстрая, средняя, медленная. А вот список results собран в порядке аргументов gather — медленная, быстрая, средняя. Это удобно: вы всегда знаете, какой индекс какому запросу принадлежит. В настоящем боте gather рассылает сообщение в несколько чатов или запрашивает данные у трёх сервисов одновременно — вместо трёх последовательных ожиданий одно, самое долгое.
create_task: задача, которая живёт сама
gather ждёт все задачи до конца — но не всякая работа должна кого-то держать. Боту часто нужна фоновая задача: напомнить через час, отправить отчёт в полночь, докачать файл после того, как хендлер уже ответил пользователю. Для этого у asyncio есть create_task: она запускает корутину прямо сейчас и никого не заставляет ждать её окончания:
import asyncio
async def reminder(text):
await asyncio.sleep(0.05)
print("Напоминаю:", text)
async def main():
task = asyncio.create_task(reminder("чай готов")) # уже работает
print("Хендлер закончился, задача живёт дальше")
await task # дожидаемся перед выходом
print("Задача завершилась")
asyncio.run(main())
Хендлер закончился, задача живёт дальше Напоминаю: чай готов Задача завершилась
Смотрите на порядок: «Хендлер закончился» напечаталось раньше напоминания — main не ждала задачу, напоминание крутилось само. В настоящем боте create_task запускает фоновые работы из хендлера: пользователь получил ответ мгновенно, а отправка отчёта уехала в фон. Одно правило гигиены: задачу нужно дождаться с await до завершения работы бота — брошенная задача умрёт вместе с циклом событий, и asyncio встретит вас предупреждением в логах.
time.sleep в хендлере: бот-статуя
Финальный эксперимент — тот же самый бот, но с ловушкой внутри. Сравните две версии рядом: asyncio.sleep и time.sleep с одинаковой задержкой. Запустите и посмотрите на разницу порядков:
import asyncio
import time
async def chat(user, blocking):
print(user + ": запрос получен")
if blocking:
time.sleep(0.2) # блокирует ВЕСЬ цикл событий
else:
await asyncio.sleep(0.2) # ждёт только эта корутина
print(user + ": ответ отправлен")
async def main():
print("--- asyncio.sleep: неблокирующе ---")
await asyncio.gather(chat("Аня", False), chat("Борис", False))
print("--- time.sleep: блокирующе ---")
await asyncio.gather(chat("Аня", True), chat("Борис", True))
asyncio.run(main())
--- asyncio.sleep: неблокирующе --- Аня: запрос получен Борис: запрос получен Аня: ответ отправлен Борис: ответ отправлен --- time.sleep: блокирующе --- Аня: запрос получен Аня: ответ отправлен Борис: запрос получен Борис: ответ отправлен
Одинаковые паузы по 0.2 секунды — разная жизнь. Вверху корутины перемешались: бот обслуживал обоих. Внизу — синхронная картина: пока time.sleep спал, цикл событий был заперт, и Борис не мог даже начать. В настоящем боте time.sleep в хендлере выглядит так: один пользователь попросил «напомни через минуту», вы написали time.sleep(60) — и весь бот на минуту умирает для всех остальных, включая команды и кнопки.
time.sleep— любая пауза в хендлере: заменяется наawait asyncio.sleep;- requests.get и любой синхронный HTTP — заменяется на
aiohttpилиhttpx.AsyncClientсawait; - тяжёлые вычисления — большой цикл, обработка файла: разбивать на куски с
await asyncio.sleep(0)или выносить в отдельный поток; - синхронные драйверы баз данных с долгими запросами — брать асинхронные аналоги или держать запросы короткими.
Как это выглядит в настоящем aiogram
В aiogram всё из этого урока уже спрятано в каркас бота: цикл событий запускает start_polling, каждый апдейт — корутина, поэтому хендлеры пишутся с async def и каждым обращением к API с await. Вот честный пример «напоминания»: бот ждёт пять секунд — но через await asyncio.sleep, поэтому остальные пользователи за эти пять секунд работают как ни в чём не бывало:
import asyncio
from aiogram import Router
from aiogram.types import Message
router = Router()
@router.message(Command("tea"))
async def tea_time(message: Message, bot) -> None:
await message.answer("Завариваю чай... осталось 5 секунд")
await asyncio.sleep(5) # ждёт только этот хендлер
await bot.send_message(message.chat.id, "Чай готов!")
@router.message(Command("tea"))
async def tea_time_bad(message: Message, bot) -> None:
await message.answer("Завариваю чай... осталось 5 секунд")
time.sleep(5) # КАТАСТРОФА: стоит весь бот
await bot.send_message(message.chat.id, "Чай готов!")
Асинхронность — не украшение aiogram, а его двигатель: polling, middleware и ваши хендлеры крутятся в одном цикле событий. Дальше по курсу этот механизм станет осязаемым: в уроке про middleware код будет вставляться в сам поток апдейтов, и без понимания await там делать нечего. Фоновые задачи из этого урока там же превратятся в троттлинг — ограничение частоты сообщений. А если хотите увидеть тот же async/await в другой роли — второй урок курса FastAPI показывает асинхронный веб-сервер uvicorn, который живёт на том же цикле событий.
Асинхронный бот не быстрее считает — он умнее ждёт: в каждом ожидании он успевает обслужить кого-то ещё.
Сначала предскажи ответ в голове — это главный навык программиста.
import asyncio
async def task(name):
print(name, "старт")
await asyncio.sleep(0)
print(name, "финиш")
async def main():
await asyncio.gather(task("А"), task("Б"))
asyncio.run(main())
def work(name):
print(name, "начал")
print(name, "закончил")
work("X")
work("Y")
import asyncio
async def slow():
await asyncio.sleep(0.05)
return "медленно"
async def fast():
await asyncio.sleep(0.01)
return "быстро"
async def main():
print(await asyncio.gather(slow(), fast()))
asyncio.run(main())
1. Что делает asyncio.run(greet("Аня"))?
2. Чем asyncio.sleep отличается от time.sleep?
3. Корутины в gather закончились в порядке Б, В, А. В каком порядке gather вернёт их результаты?
4. В хендлере написали bot.send_message(chat_id, text) без await. Что произойдёт?
5. Почему time.sleep(5) внутри хендлера aiogram — катастрофа?
Соберите мини-стенд параллельного бота: корутина chat(user) печатает «получила сообщение», делает await asyncio.sleep(0), затем печатает «ответила». Запустите chat("Аня") и chat("Борис") одновременно через gather в main() и запустите main через asyncio.run.
Зачем aiogram асинхронный?
Бот почти никогда не считает — он ждёт ответы сети. Асинхронность позволяет ждать сразу для тысяч пользователей: пока один хендлер стоит на await, цикл событий обслуживает остальных. Синхронный бот обрабатывает сообщения по очереди, и при десятках одновременных пользователей очередь становится ощутимой.
Чем asyncio.sleep отличается от time.sleep простыми словами?
time.sleep останавливает всю программу: пока он спит, бот не отвечает никому. asyncio.sleep ставит на паузу только одну корутину и вежливо отдаёт управление остальным. В хендлерах aiogram допустим только asyncio.sleep и только с await.
Что делать, если боту нужен блокирующий код: requests или тяжёлые вычисления?
Заменить на асинхронные аналоги: aiohttp или httpx.AsyncClient вместо requests с await на каждом запросе. Тяжёлые вычисления разбивают на куски с await asyncio.sleep(0) между ними или выносят в отдельный поток через loop.run_in_executor. Держать долгий синхронный вызов прямо в хендлере нельзя — он заморозит бот для всех.
Что такое корутина простыми словами?
Функция с правом на паузу: в отмеченных местах (await) она может прерваться и дать поработать другим, а потом продолжиться с того же места. Вызов корутины не выполняет её, а создаёт объект; запускают корутины asyncio.run, await или gather. В aiogram каждый хендлер — корутина, поэтому хендлеры пишут через async def.
Понравился урок? Сошлитесь на него
«time.sleep останавливает весь цикл событий, а asyncio.sleep ставит на паузу только одну корутину.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
FastAPI · Урок 2
Первое приложение на FastAPI: маршруты, uvicorn и Swagger
Устанавливаем FastAPI, пишем первый эндпоинт, запускаем сервер uvicorn и открываем Swagger — документацию, которая пишет себя сама.
aiogram · Урок 8
Middleware в aiogram: антиспам, логирование и общие данные
Пишем middleware в aiogram: антиспам-троттлинг, логирование всех апдейтов и передачу общих данных в хендлеры — механику запускаем прямо на странице.
aiogram · Урок 6
База данных в телеграм-боте: SQLite от первого лица
Даём боту настоящую память: таблица пользователей с chat_id, запись через INSERT OR IGNORE, чтение по chat_id и параметр ? против SQL-инъекций.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
FastAPI · Урок 1
Что такое API и REST: введение в FastAPI для начинающих
Понять, что такое API, REST, HTTP-методы и JSON — и подготовиться к первому приложению на FastAPI.
rest api для начинающихfastapi для начинающих
NumPy · Урок 1
Что такое NumPy и как установить через pip: первый массив ndarray
Первый массив ndarray: создаём, сравниваем со списком, разбираем dtype и shape — и ускоряем сумму миллиона чисел примерно в сто раз.
numpy для начинающихчто такое numpy простыми словами
aiogram · Урок 3
Клавиатуры в телеграм-боте: reply и inline кнопки в aiogram
Строим кнопки, которыми приятно пользоваться: ReplyKeyboardMarkup против InlineKeyboardMarkup, ряды, callback_data и обработка нажатий в aiogram 3.
inline кнопки aiogramклавиатура телеграм бота