Async/Await в Python: Просто о сложном

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

1. Синхронность против Асинхронности: Аналогия с приготовлением завтрака и очередью в кофейне

Синхронность против Асинхронности: Аналогия с приготовлением завтрака и очередью в кофейне

Добро пожаловать в курс Async/Await в Python: Просто о сложном. Если вы когда-либо открывали документацию по asyncio и чувствовали, что ваш мозг начинает закипать от терминов вроде «цикл событий», «футуры» и «корутины», то вы попали по адресу.

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

Эта первая статья — фундамент. Прежде чем писать код, нам нужно почувствовать разницу между синхронным (обычным) и асинхронным подходами.

Что такое Синхронность? (Очередь в кофейне)

Представьте себе маленькую уютную кофейню. В ней работает один бариста, и есть одна касса.

Утро, люди спешат на работу, выстроилась очередь.

  • Подходит первый клиент, заказывает сложный латте с карамелью.
  • Бариста принимает заказ.
  • Бариста идет к кофемашине, мелет зерна, взбивает молоко, рисует сердечко на пенке.
  • Бариста отдает кофе клиенту.
  • Только после этого он поворачивается ко второму клиенту и спрашивает: «Что будете заказывать?».
  • Это и есть синхронное выполнение (Synchronous execution).

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

    Пример синхронного кода

    Давайте напишем простой скрипт на Python, который имитирует эту кофейню. Мы будем использовать функцию time.sleep(), чтобы показать задержку (время на приготовление).

    Разбор кода:

  • import time: Мы импортируем модуль для работы со временем.
  • time.sleep(3): Эта команда полностью останавливает выполнение программы на 3 секунды. Это называется блокирующая операция. Пока она работает, Python не может делать ничего другого.
  • Мы вызываем функцию три раза подряд.
  • Результат: Программа потратит примерно 9 секунд. Почему? Потому что . Бариста не начинал второй заказ, пока не закончил первый.

    Что такое Асинхронность? (Приготовление завтрака)

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

    Если бы вы готовили синхронно, это выглядело бы так:

  • Положили хлеб в тостер. Стоите и смотрите на тостер 2 минуты, пока хлеб не поджарится. Достали тост.
  • Включили чайник. Стоите и смотрите на чайник 3 минуты, пока он не закипит. Налили кофе.
  • Разбили яйца на сковороду. Стоите и смотрите на сковороду 5 минут. Сняли яичницу.
  • Звучит глупо, правда? В реальной жизни мы действуем асинхронно.

    Асинхронный алгоритм:

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

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

    Пример асинхронного кода

    В Python для этого используются ключевые слова async и await. Давайте перепишем наш пример, используя библиотеку asyncio.

    Разбор кода:

  • async def: Это объявление асинхронной функции (корутины). Она умеет приостанавливать свое выполнение.
  • await: Это ключевое слово говорит Python: «Здесь будет долгая операция. Не стой, иди займись чем-нибудь другим, пока я жду».
  • asyncio.sleep(3): В отличие от time.sleep(), эта функция не блокирует всю программу. Она просто говорит системе: «Разбуди меня через 3 секунды».
  • asyncio.gather(...): Эта команда собирает несколько задач и запускает их конкурентно.
  • Результат: Программа потратит всего чуть больше 3 секунд!

    Почему? Потому что мы запустили приготовление кофе для первого клиента, и пока машина гудела (ожидание), мы тут же запустили для второго и третьего. Все три чашки готовились параллельно.

    Ключевые понятия простыми словами

    Чтобы двигаться дальше по курсу, запомните эти три термина:

  • Корутина (Coroutine): Это функция, объявленная через async def. Это как задача в вашем списке дел («Пожарить тост»). Ее можно поставить на паузу и вернуться к ней позже.
  • Await: Команда «Ждать, но не блокировать». Вы говорите: «Я подожду, пока закипит чайник, а пока пойду нарежу сыр».
  • Event Loop (Цикл событий): Это «Менеджер» вашей кухни. Он бегает между тостером, чайником и сковородой, проверяя, что уже готово, и решает, что делать дальше. В коде его запускает asyncio.run().
  • Когда нужен Async, а когда нет?

    Многие новички думают: «О, асинхронность быстрее! Буду использовать её везде!». Это ошибка.

    Асинхронность полезна не всегда. Давайте разберемся с помощью двух типов задач.

    1. I/O Bound (Задачи, связанные с вводом-выводом)

    Это задачи, где программа в основном ждет ответа от чего-то внешнего: * Скачивание файлов из интернета. * Запросы к базе данных. * Чтение/запись больших файлов на диск. * Ожидание ответа от API (например, бота в Telegram).

    > Здесь асинхронность — король. Пока мы ждем ответа от сервера, мы можем отправить еще 100 запросов.

    2. CPU Bound (Задачи, нагружающие процессор)

    Это задачи, где программа активно считает: * Обработка видео или изображений. * Сложные математические вычисления. * Обучение нейросетей. * Архивация данных.

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

    Практические рекомендации: Чек-лист

    Как понять, нужен ли вам async в конкретной задаче? Задайте себе вопрос:

    «Что делает моя программа большую часть времени?»

    * Если ответ: «Ждет» (сеть, диск, база данных) Используйте Async/Await. * Если ответ: «Думает/Считает» (математика, циклы for на миллион итераций) Используйте обычный синхронный код или Multiprocessing (о котором мы поговорим в продвинутых курсах).

    Итог первой статьи

    Мы выяснили, что: * Синхронность — это как очередь в один магазин: стоим и ждем. * Асинхронность — это как готовка на кухне: пока варится одно, делаем другое. * async и await — это инструменты, позволяющие Python эффективно управлять временем ожидания. * Асинхронность идеально подходит для задач, связанных с сетью и вводом-выводом (I/O Bound).

    В следующей статье мы подробно разберем синтаксис async def и узнаем, почему нельзя просто так взять и добавить await в любую функцию.

    2. Основы синтаксиса: Как работают ключевые слова async и await и запуск событийного цикла

    Основы синтаксиса: Как работают ключевые слова async и await и запуск событийного цикла

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

    Теперь пришло время отложить фартуки и надеть мантию программиста. Сегодня мы разберем «магические слова» Python: async и await, а также познакомимся с главным дирижером всего оркестра — Циклом событий (Event Loop).

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

    1. Ключевое слово async: Создание плана действий

    В обычном (синхронном) Python, когда вы пишете функцию через def, вы говорите интерпретатору: «Вот инструкция. Как только я ее вызову — выполняй немедленно и до конца».

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

    Что изменилось?

    Самое интересное происходит, когда вы пытаетесь вызвать эти функции.

    Если вы вызовете cook_toast(), Python тут же напечатает текст. Но если вы вызовете cook_toast_async(), ничего не произойдет. Точнее, код внутри функции не выполнится.

    Вместо выполнения Python вернет вам объект корутины (coroutine object).

    > Аналогия: > Представьте, что обычная функция — это когда вы говорите повару: «Жарь котлету!», и он тут же начинает жарить. > Асинхронная функция (async def) — это когда вы пишете на бумажке «Инструкция: пожарить котлету» и отдаете этот листок повару. Повар берет листок, кивает и... кладет его в карман. Он еще ничего не сделал. Он просто получил план действий.

    Этот «план действий» в Python и называется корутиной.

    2. Ключевое слово await: Точка переключения

    Итак, у нас есть корутина (план действий). Но как заставить её работать? И, что еще важнее, как сказать Python: «Здесь будет долго, можешь пока заняться чем-то другим»?

    Для этого существует слово await (от англ. wait — ждать).

    await можно использовать только внутри функций, определенных через async def.

    Когда Python встречает await, происходит магия:

  • Выполнение текущей функции приостанавливается.
  • Управление передается обратно в систему (Цикл событий).
  • Система запоминает, где мы остановились, и идет выполнять другие задачи.
  • Когда ожидаемое действие завершается (например, таймер истек или данные скачались), система возвращается к этой строчке и продолжает выполнение.
  • Если бы мы использовали time.sleep(2), программа бы «зависла». С await asyncio.sleep(2) программа говорит: «Я вернусь к этому месту через 2 секунды, а пока я свободна».

    3. Event Loop (Цикл событий): Главный менеджер

    Мы написали функции с async, расставили await. Но кто всем этим управляет? Кто решает, какую задачу выполнять сейчас, а какую отложить?

    Знакомьтесь: Event Loop (Цикл событий).

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

    Цикл событий — это бесконечный цикл, который:

  • Смотрит, есть ли задачи, готовые к выполнению.
  • Запускает задачу.
  • Если задача встречает await, цикл оставляет её «дозревать» и переходит к следующей.
  • Если задача завершилась, он отмечает её выполненной.
  • В старых версиях Python нужно было создавать этот цикл вручную. Сейчас всё проще.

    Запуск через asyncio.run()

    Это кнопка «Старт» для вашей асинхронной программы. Эта функция создает новый цикл событий, запускает в нем вашу главную корутину и закрывает цикл, когда всё готово.

    Давайте соберем всё вместе в работающий скрипт.

    Что произойдет по шагам:

  • asyncio.run(main()) создает Цикл событий и кидает в него задачу main.
  • main начинает выполняться, печатает "Запуск программы".
  • Встречает await say_hello_delayed(). main ставится на паузу, управление переходит внутрь say_hello_delayed.
  • Печатается "Привет...".
  • Встречается await asyncio.sleep(1). Функция засыпает, но Цикл событий не блокируется (хотя в данном простом примере других задач нет, поэтому он просто ждет).
  • Через 1 секунду управление возвращается, печатается "...мир!".
  • say_hello_delayed завершается. Управление возвращается в main.
  • Печатается "Программа завершена".
  • 4. Распространенные ошибки новичков

    При переходе на async/await легко наступить на грабли. Вот две самые частые ошибки.

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

    Если вы вызовете асинхронную функцию без await, Python не выдаст ошибку сразу, но выдаст предупреждение: RuntimeWarning: coroutine '...' was never awaited. Код внутри функции просто не выполнится.

    Ошибка №2: Использование блокирующих функций

    Это смертный грех в асинхронном программировании.

    Если вы используете time.sleep() или тяжелые вычисления внутри async def, вы останавливаете весь Цикл событий. Никакие другие задачи (даже если их сотни) не смогут выполняться, пока этот блокирующий код не завершится. Ваш бариста просто ушел курить, пока очередь ждет.

    Немного математики: Эффективность

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

    При синхронном (последовательном) выполнении задач, время выполнения равно сумме времени каждой задачи:

    Где: * — общее время выполнения в синхронном режиме. * — количество задач. * — время выполнения -й задачи.

    При асинхронном (конкурентном) выполнении задач, которые состоят в основном из ожидания (I/O bound), общее время стремится к времени самой долгой задачи:

    Где: * — общее время выполнения в асинхронном режиме. * — функция выбора максимального значения из набора.

    Пример: У нас есть 3 запроса к сайту, каждый длится 2 секунды. * Синхронно: секунд. * Асинхронно: секунды (плюс небольшие накладные расходы на переключение).

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

    Как понять, что вы всё делаете правильно?

  • Вирусность: Если вы используете await в одной функции, то функция, которая её вызывает, тоже должна быть async, и в ней тоже должен быть await. Асинхронность распространяется вверх по коду как вирус, до самого asyncio.run().
  • Библиотеки: Используйте специальные асинхронные библиотеки. Нельзя использовать стандартный requests внутри async def (он блокирующий). Вместо него используют aiohttp или httpx.
  • Базы данных: То же самое. Стандартный psycopg2 или sqlite3 заблокируют цикл. Нужны асинхронные драйверы (например, asyncpg или aiosqlite).
  • Итог

    * async def объявляет функцию, которая может быть поставлена на паузу. * Вызов такой функции не запускает её, а возвращает объект корутины. * await запускает корутину и ждет её завершения, позволяя Циклу событий переключиться на другие дела. * asyncio.run() — это точка входа, которая запускает Цикл событий. * Никогда не используйте блокирующие операции (time.sleep) внутри асинхронных функций.

    В следующей статье мы научимся запускать много задач одновременно с помощью asyncio.gather и посмотрим на реальную мощь этого подхода.

    3. Ускоряемся: Конкурентное выполнение нескольких задач одновременно с помощью asyncio.gather

    Ускоряемся: Конкурентное выполнение нескольких задач одновременно с помощью asyncio.gather

    Приветствую вас, коллеги! Мы продолжаем наш курс Async/Await в Python: Просто о сложном.

    В прошлых статьях мы научились создавать асинхронные функции (async def) и запускать их (await). Мы поняли, что await позволяет передать управление, пока мы чего-то ждем. Но если вы внимательно смотрели на код из предыдущего урока, то могли заметить одну деталь: мы запускали задачи по очереди.

    Сегодня мы сделаем настоящий прорыв. Мы научимся запускать задачи одновременно (конкурентно). Именно здесь кроется та самая невероятная скорость асинхронного Python, о которой все говорят.

    Готовы? Тогда поехали!

    Проблема последовательного await

    Давайте вернемся на нашу воображаемую кухню. Представьте, что вам нужно приготовить завтрак: сварить кофе (3 секунды) и пожарить тост (3 секунды).

    Используя знания из прошлой статьи, вы могли бы написать такой код:

    Что здесь происходит?

  • Python видит await brew_coffee(). Он запускает функцию и ждет, пока она полностью завершится.
  • Проходит 3 секунды.
  • Только потом он переходит к await toast_bread() и снова ждет 3 секунды.
  • Результат: 6 секунд. Это синхронное поведение внутри асинхронной функции. Мы не получили никакого выигрыша в скорости! Это все равно, что стоять и смотреть на кофемашину, и только когда она закончит, подойти к тостеру.

    Решение: asyncio.gather

    Чтобы делать дела одновременно, нам нужен инструмент, который скажет Python: «Запусти эти задачи вместе и сообщи мне, когда все они будут готовы».

    Этот инструмент — функция asyncio.gather.

    Давайте перепишем нашу функцию main:

    Результат: ~3 секунды!

    Как это работает?

    asyncio.gather принимает список корутин (ваших задач) и планирует их выполнение в Цикле событий (Event Loop) практически одновременно.

  • gather запускает brew_coffee(). Функция доходит до await sleep и отдает управление.
  • gather тут же, не теряя времени, запускает toast_bread(). Она тоже доходит до await sleep и отдает управление.
  • Обе задачи «спят» параллельно.
  • Через 3 секунды обе просыпаются, и gather собирает их результаты в один список.
  • !Сравнение последовательного и конкурентного выполнения задач во времени

    Возвращаемые значения

    Обратите внимание на переменную results в коде выше.

    asyncio.gather возвращает список результатов. Самое приятное — порядок результатов в списке строго соответствует порядку задач, которые вы передали в аргументы, а не тому, кто быстрее закончил.

    Пример:

    Даже если task_fast выполнится за 0.1 секунды, а task_slow за 10 секунд, в переменной res первым элементом (res[0]) все равно будет результат task_slow.

    Вы можете сразу распаковывать результаты:

    Обработка ошибок: Что если тостер взорвется?

    Это важный момент. По умолчанию, если одна из задач внутри gather упадет с ошибкой (Exception), то gather немедленно прервет ожидание и выбросит эту ошибку в main. Остальные задачи при этом могут быть отменены или продолжить работу в фоне (зависит от версии Python и настроек), но вы потеряете контроль над их результатами.

    Иногда нам нужно, чтобы программа продолжила работу, даже если одна задача провалилась. Для этого есть параметр return_exceptions=True.

    С флагом return_exceptions=True вместо падения программы вы получите объект ошибки прямо в списке результатов.

    Немного математики: Оценка эффективности

    Давайте формализуем наш выигрыш во времени. Это поможет вам принимать решения при проектировании архитектуры.

    Пусть у нас есть задач, и время выполнения -й задачи равно .

    При последовательном выполнении (через обычный await в цикле) общее время равно сумме всех времен:

    Где: * — общее время последовательного выполнения. * — знак суммы. * — время выполнения конкретной задачи.

    При конкурентном выполнении через asyncio.gather (при условии достаточных ресурсов и I/O задач) общее время стремится к времени самой долгой задачи:

    Где: * — общее время конкурентного выполнения. * — функция выбора максимального значения из набора.

    > Пример: Если вам нужно скачать 100 картинок, и каждая качается 1 секунду. > * Последовательно: 100 секунд. > * Конкурентно: ~1 секунда (плюс накладные расходы на работу сети и процессора, реально может быть 1.5-2 секунды, но это все равно в 50 раз быстрее!).

    Практический пример: Скачиваем "данные" из интернета

    Давайте напишем имитацию реальной задачи: получение данных о пользователях с разных серверов.

    В этом примере мы создали список корутин и передали их в gather. Это самый частый паттерн использования: сформировать список задач в цикле, а потом разом их выполнить.

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

    Как понять, нужен ли вам gather?

  • Независимость задач: Если результат второй задачи не зависит от результата первой, их можно и нужно запускать параллельно. (Пример: скачать погоду и скачать курс валют — они не связаны).
  • Зависимость: Если для второй задачи нужны данные из первой (например, сначала скачать ID пользователя, а потом по этому ID найти его фото), то gather здесь не поможет. Придется использовать последовательные await.
  • Массовые операции: Если у вас есть цикл for, внутри которого стоит await, почти всегда это можно переписать на gather и ускорить в разы.
  • Чек-лист перед использованием:

    * [ ] Задачи не зависят друг от друга? * [ ] Задач больше одной? * [ ] Это I/O операции (сеть, диск, ожидание)?

    Если везде «Да» — смело используйте asyncio.gather!

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

    4. Главная ловушка: Блокирующие операции и почему time.sleep убивает всю асинхронность

    Главная ловушка: Блокирующие операции и почему time.sleep убивает всю асинхронность

    Приветствую вас, друзья! Мы продолжаем наш курс Async/Await в Python: Просто о сложном.

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

    Но здесь кроется главная ловушка, в которую попадают 99% новичков. Одна маленькая ошибка может превратить ваш супер-быстрый асинхронный код в обычный, медленный, синхронный скрипт (а иногда сделать его даже медленнее).

    Сегодня мы поговорим о блокирующих операциях — убийцах асинхронности.

    Аналогия: Зависший Бариста

    Вспомним нашу любимую кофейню.

    Идеальная асинхронность: Бариста (Event Loop) принимает заказ, включает кофемашину и, пока она гудит, поворачивается к следующему клиенту. Он постоянно в движении, переключаясь между задачами.

    Блокирующая операция: Представьте, что посреди рабочего дня бариста решил разгадать кроссворд.

  • Он берет газету.
  • Он перестает принимать заказы.
  • Он перестает следить за кофемашиной (кофе убегает).
  • Он перестает отдавать готовые напитки.
  • Вся кофейня ждет, пока он не допишет слово в кроссворде.
  • В программировании это называется блокировкой Цикла событий (Blocking the Event Loop).

    !Сравнение работающего цикла событий и заблокированного цикла событий.

    Враг №1: time.sleep()

    Самый простой способ «убить» асинхронность — использовать стандартную функцию time.sleep().

    Давайте посмотрим на код, который выглядит асинхронным, но на самом деле таковым не является.

    Результат выполнения:

    Почему так произошло?

    Мы использовали asyncio.gather, мы написали async def. Почему программа работала 6 секунд (), а не 2 секунды?

    Потому что time.sleep(2) — это синхронная функция операционной системы. Когда Python встречает её, он останавливает весь поток (thread).

    А так как asyncio работает в одном потоке, то останавливается и сам Цикл событий (Event Loop). Он просто не может переключиться на другую задачу, потому что он «спит» вместе с первой функцией.

    Как исправить?

    Нужно использовать неблокирующий аналог, который «отпускает» управление, но не останавливает поток.

    Если заменить time.sleep(2) на await asyncio.sleep(2), время выполнения станет 2.01 сек. Цикл событий увидит await, поймет, что нужно подождать, и тут же возьмет следующую задачу.

    Скрытые враги: Библиотеки

    time.sleep — это очевидный пример. В реальной жизни вы вряд ли будете специально усыплять программу. Гораздо опаснее использование синхронных библиотек внутри асинхронного кода.

    Пример с requests

    Библиотека requests — стандарт де-факто для работы с сетью в Python. Но она синхронная (блокирующая).

    Если вы напишете такой код:

    То пока ваш компьютер ждет ответа от сайта (например, 1 секунду), вся ваша программа зависнет. Никакие другие пользователи не получат ответ, никакие таймеры не сработают.

    Правило: Внутри async def нельзя использовать функции, которые долго ждут ввода-вывода (I/O) и не поддерживают await.

    Решение: Используйте асинхронные альтернативы.

    | Синхронная библиотека (Нельзя) | Асинхронная альтернатива (Нужно) | | :--- | :--- | | time.sleep() | asyncio.sleep() | | requests | aiohttp или httpx | | psycopg2 (PostgreSQL) | asyncpg | | sqlite3 | aiosqlite | | open() (чтение файлов) | aiofiles |

    Тяжелые вычисления (CPU Bound)

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

    Представьте, что вы решили посчитать сумму чисел от 1 до 100 миллионов внутри асинхронной функции.

    Даже если вы запустите это через asyncio.gather, Цикл событий заблокируется. Почему? Потому что Python выполняет код строчка за строчкой. Пока он крутит этот огромный цикл for, он не может вернуться в Event Loop и проверить, есть ли другие задачи.

    > Важно: async/await не делает вычисления быстрее. Он делает эффективным ожидание. Если ваша задача — молотить числа процессором, asyncio вам не поможет (здесь нужен multiprocessing, но это тема другого курса).

    Математика блокировки

    Давайте выразим влияние блокирующей операции на пропускную способность системы формулой.

    Пусть — это количество запросов, которые ваш сервер может обработать в секунду (RPS - Requests Per Second).

    В идеальной асинхронной модели:

    где — время, которое процессор тратит на обработку кода (очень маленькое число).

    Если мы добавляем блокирующую операцию длительностью (например, time.sleep(1)), формула меняется:

    Где: * — пропускная способность с блокировкой. * — время работы процессора (например, 0.001 сек). * — время блокировки (например, 1.0 сек).

    Пример: Без блокировки: запросов в секунду. С блокировкой на 1 секунду: запросов в секунду.

    Вывод: Одна блокирующая секунда снизила производительность в 1000 раз! Ваш сервер превратился из гоночного болида в черепаху.

    Практические рекомендации: Чек-лист

    Как понять, что вы написали плохой код?

  • Поиск по коду: Нажмите Ctrl+F и поищите time.sleep. Если нашли внутри async def — это ошибка.
  • Сторонние библиотеки: Если вы используете библиотеку внутри async def, зайдите в её документацию. Если там нет примеров с await — скорее всего, она блокирующая.
  • Циклы: Если у вас есть циклы for или while на огромное количество итераций без await внутри — это заблокирует цикл.
  • Логирование: Если вы видите, что ваша программа "замирает" и не пишет логи какое-то время, а потом выплевывает всё сразу — где-то есть блокировка.
  • Что делать, если ОЧЕНЬ нужно?

    Иногда у нас нет выбора. Например, есть старая библиотека для работы с редким оборудованием, и она только синхронная. Не переписывать же её с нуля?

    В asyncio есть специальный метод run_in_executor. Он позволяет запустить блокирующую функцию в отдельном потоке (thread), не останавливая основной Цикл событий.

    Но используйте это как крайнюю меру. Лучший путь — всегда искать асинхронные ("родные") библиотеки.

    Итог

    * Асинхронность хрупка. Одной синхронной команды достаточно, чтобы остановить всю систему. * Никакого time.sleep. Используйте await asyncio.sleep. * Никакого requests. Используйте aiohttp. * Event Loop один. Если вы занимаете его долгой работой или ожиданием, остальные задачи стоят в очереди.

    Теперь вы знаете, как не сломать свой код. В следующей, заключительной статье курса, мы соберем всё вместе и напишем полноценный асинхронный парсер сайтов!

    5. Итоги и чек-лист: Как понять, нужен ли async в вашей конкретной задаче

    Итоги и чек-лист: Как понять, нужен ли async в вашей конкретной задаче

    Поздравляю! Вы прошли путь от новичка, который пугался слов «корутина» и «цикл событий», до разработчика, понимающего, как жонглировать задачами в Python.

    Мы разобрали аналогию с кофейней, изучили синтаксис async/await, научились ускорять код с asyncio.gather и узнали, как не убить производительность блокирующими операциями.

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

    Главный вопрос: А оно мне надо?

    Асинхронность — это не волшебная пыль, которая делает любой код быстрее. Это инструмент для конкретного типа задач. Если вы попытаетесь забивать гвозди микроскопом (использовать asyncio для математических вычислений), вы не только не ускорите работу, но и сделаете её медленнее и сложнее.

    Давайте раз и навсегда разделим задачи на две категории.

    1. I/O Bound (Ограниченные вводом-выводом)

    Это задачи, где «узким местом» является не ваш процессор, а внешняя система.

    * Признаки: Программа большую часть времени ничего не делает, а просто ждет. Ждет ответа от сайта, ждет записи файла на диск, ждет сообщения из Telegram. * Вердикт: Здесь asyncioидеальный выбор. Пока мы ждем, мы можем запустить тысячи других таких же ожидающих задач.

    2. CPU Bound (Ограниченные процессором)

    Это задачи, где «узким местом» является скорость вашего CPU.

    * Признаки: Процессор греется, кулеры шумят. Программа считает хеши, обрабатывает видео, обучает нейросети, перемножает матрицы. * Вердикт: Здесь asyncio бесполезен. Если одно ядро процессора занято вычислениями, переключение контекста только добавит накладных расходов.

    !Блок-схема для выбора между асинхронным и синхронным подходами

    Чек-лист: Принимаем решение за 3 шага

    Перед началом нового проекта или написанием функции пройдитесь по этому списку.

    Шаг 1. Анализ природы задачи

    Задайте себе вопрос: «Если я куплю процессор в 2 раза мощнее, моя программа станет работать в 2 раза быстрее?»

    * Да Это CPU Bound. Асинхронность не нужна. Используйте модуль multiprocessing (параллельные процессы) или обычный синхронный код. * Нет (потому что всё равно придется ждать ответа от сервера) Это I/O Bound. Асинхронность нужна.

    Шаг 2. Масштаб задачи

    Сколько таких операций вам нужно выполнить?

    * Одна-две: (Например, скачать один файл). Можно обойтись обычным синхронным кодом. Выигрыш от асинхронности будет незаметен на фоне сложности настройки. * Сотни и тысячи: (Например, проверить доступность 1000 сайтов). Однозначно Async/Await. Синхронный код будет работать вечность.

    Шаг 3. Наличие библиотек

    Есть ли для вашей задачи асинхронная библиотека?

    * Если вы работаете с HTTP, есть aiohttp или httpx. * Если с базой данных Postgres, есть asyncpg. * Если с файлами, есть aiofiles.

    > Если для вашей специфической задачи (например, драйвер редкого сканера штрих-кодов) есть только старая синхронная библиотека, внедрение asyncio может стать болью. Вам придется запускать её в отдельных потоках через run_in_executor.

    Финальный проект: Асинхронный краулер

    Давайте закрепим все знания курса, написав прототип реального приложения. Мы создадим скрипт, который «скачивает» данные с нескольких URL-адресов.

    Чтобы код можно было запустить прямо сейчас без установки внешних библиотек (вроде aiohttp), мы будем имитировать сетевые запросы через asyncio.sleep, но структура кода будет полностью соответствовать боевому приложению.

    Задача

    У нас есть список из 5 «сайтов». Нам нужно получить данные с каждого. Некоторые сайты отвечают быстро, некоторые медленно, а некоторые могут вернуть ошибку.

    Разбор полетов

  • Конкурентность: Мы запустили 5 запросов. Если бы мы делали это синхронно, и каждый запрос занимал в среднем 2.5 секунды, мы бы ждали секунд. В асинхронном варианте мы ждем столько, сколько отвечает самый медленный сайт (максимум 4 секунды).
  • Обработка ошибок: Благодаря return_exceptions=True в gather, падение одного запроса не сломало остальные. Это критически важно для надежных систем.
  • Масштабируемость: Этот код легко переварит и 5, и 50, и 500 ссылок (с небольшими доработками по ограничению одновременных запросов).
  • Немного математики: Коэффициент ускорения

    Чтобы оценить эффективность внедрения асинхронности, используют понятие Speedup (Ускорение).

    Где: * — коэффициент ускорения (во сколько раз быстрее). * — время выполнения программы в синхронном режиме. * — время выполнения программы в асинхронном режиме.

    В идеальном случае для I/O задач, если мы запускаем задач параллельно, стремится к . Однако в реальности существуют накладные расходы на работу Цикла событий, поэтому ускорение всегда чуть меньше количества задач.

    Итоговые заповеди асинхронного питониста

    Чтобы ваш код был чистым и быстрым, запомните эти 5 правил:

  • Async заразен. Если вы используете await в глубине программы, все функции выше по цепочке вызовов тоже должны стать async, вплоть до asyncio.run().
  • Не блокируй. Никогда не используй time.sleep() или тяжелые вычисления внутри async def. Это остановит сердце вашей программы — Event Loop.
  • Используй Gather. Запускай независимые задачи через asyncio.gather, чтобы они выполнялись параллельно, а не последовательно.
  • Правильные библиотеки. Забудь про requests и стандартные драйверы БД. Ищи библиотеки с префиксом aio (aiohttp, aiopg и т.д.).
  • Не усложняй. Если скрипт простой и линейный — пиши синхронно. Асинхронность добавляет сложность в отладке и тестировании. Используй её там, где она действительно нужна.
  • Что дальше?

    Мы изучили основы, которых хватит для решения 90% задач. Но мир асинхронности глубже. Если захотите развиваться дальше, вот темы для самостоятельного изучения:

    * Asyncio Queues (Очереди): Как организовать конвейер обработки данных между разными корутинами (Producer-Consumer паттерн). * Locks и Semaphores (Примитивы синхронизации): Как ограничить количество одновременных запросов к сайту (чтобы вас не забанили) и как избежать гонки данных. * FastAPI: Самый популярный современный веб-фреймворк, который полностью построен на async/await. Это лучшее место для применения ваших новых знаний на практике.

    Спасибо, что прошли этот курс! Теперь вы готовы писать быстрый и современный код на Python. Удачи в экспериментах!