Асинхронное программирование в Python: asyncio – Основы

В предыдущих статьях мы рассмотрели многопоточность (threading) и многопроцессорность (multiprocessing) как способы достижения конкурентности в Python. Теперь мы познакомимся с совершенно другим подходом — асинхронным программированием с использованием модуля asyncio.

asyncio позволяет писать однопоточный конкурентный код с помощью концепции корутин (coroutine). Это особенно эффективно для I/O-bound задач, где производительность может быть значительно выше, чем у многопоточных решений, за счет меньших накладных расходов.

Что такое асинхронное программирование?

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

Асинхронное программирование предлагает другой подход. Оно позволяет программе начать операцию, требующую ожидания (I/O-операцию), и, не дожидаясь ее завершения, переключиться на выполнение других задач. Когда ожидаемая операция завершится, программа сможет вернуться к обработке ее результата.

В asyncio это достигается за счет кооперативной многозадачности:

  • Задачи (корутины) сами явно сообщают, когда они готовы уступить управление (обычно когда ожидают завершения I/O-операции).
  • Цикл событий (Event Loop) — это "мозг" asyncio. Он отслеживает все активные задачи и операции ожидания. Когда одна задача уступает управление, цикл событий находит другую задачу, готовую к выполнению, и передает управление ей.

Как это увеличивает производительность для I/O-bound задач?

Увеличение производительности в asyncio происходит не за счет параллельного выполнения вычислений на нескольких ядрах CPU (как в multiprocessing), а за счет максимально эффективного использования времени процессора. Вместо того чтобы процессор простаивал, пока программа ждет ответа от сети или чтения файла, asyncio позволяет ему в это время обрабатывать другие задачи. Это означает, что программа может одновременно "держать в воздухе" множество I/O-операций, быстро переключаясь между ними по мере их готовности.

Преимущества:

  • Высокая отзывчивость: Приложение не "зависает" на долгих операциях ввода-вывода.
  • Эффективное использование ресурсов: Меньше накладных расходов по сравнению с созданием и управлением большим количеством потоков операционной системы для каждой I/O-операции.
  • Масштабируемость: Позволяет обрабатывать тысячи одновременных подключений (например, в веб-серверах) с относительно небольшим количеством системных ресурсов.

Важно: Асинхронное программирование с asyncio на одном потоке не ускорит CPU-bound задачи (задачи, интенсивно использующие процессор). Если у вас есть сложные математические расчеты, их параллельное выполнение на нескольких ядрах потребует multiprocessing. asyncio сияет именно в задачах, где много ожидания.

Ключевые концепции asyncio:

  • Цикл событий (Event Loop): Управляет выполнением асинхронных задач.
  • Корутины (Coroutines): Функции, определенные с async def, чье выполнение можно приостанавливать и возобновлять.

Ключевые слова: async и await

Начиная с Python 3.5, для асинхронного программирования были введены специальные ключевые слова:

  • async def: Используется для определения корутины (асинхронной функции).
    Python 3.13
    async def my_coroutine():
        # ... асинхронный код ...
        pass
    
  • await: Используется внутри корутины для ожидания результата другой корутины или объекта, поддерживающего протокол ожидания (awaitable, например, Future или другая корутина). Пока await ожидает, цикл событий может выполнять другие задачи.
    Python 3.13
    async def main():
        result = await another_coroutine() # Приостанавливает main, пока another_coroutine не завершится
        print(result)
    
    Использовать await можно только внутри функции, объявленной с async def.

Корутины (Coroutines)

Корутина — это основной строительный блок asyncio. Когда вы вызываете функцию, определенную с async def, она немедленно возвращает объект корутины, а не выполняет тело функции.

Python 3.13
import asyncio

async def greet(name):
    print(f"Привет, {name}!")
    await asyncio.sleep(1) # Имитация I/O операции (неблокирующий сон)
    print(f"Пока, {name}!")

# Вызов greet() возвращает объект корутины, но не выполняет ее код
coro_obj = greet("Алиса")
print(type(coro_obj)) # <class 'coroutine'>

# Чтобы запустить корутину, нужен цикл событий
# Пример запуска см. ниже с использованием asyncio.run()

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

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

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

Функция asyncio.run()

asyncio.run(coroutine) — это высокоуровневая функция (добавлена в Python 3.7+), которая упрощает запуск асинхронного кода. Она берет на себя создание цикла событий, запуск в нем переданной корутины и управление его жизненным циклом.

Python 3.13
import asyncio
import time

async def say_after(delay, what):
    await asyncio.sleep(delay) # Неблокирующий сон
    print(what)

async def main_program():
    start_time = time.time()
    print(f"Начало выполнения в {time.strftime('%X')}")

    await say_after(1, "Привет")
    await say_after(2, "Мир")

    print(f"Завершено за {time.time() - start_time:.2f} сек.")

if __name__ == "__main__":
    asyncio.run(main_program())

Обратите внимание, что asyncio.sleep() не блокирует весь поток, а позволяет циклу событий выполнять другие задачи, если они есть. В данном примере, поскольку await стоит перед каждым вызовом say_after, они выполнятся последовательно, но сама операция sleep является асинхронной.

Задачи (asyncio.Task)

Чтобы корутины выполнялись конкурентно (то есть, могли переключаться, пока одна из них ожидает), их нужно обернуть в Задачи (Tasks) с помощью asyncio.create_task() (Python 3.7+) или asyncio.ensure_future() (более старый способ).

Задача — это объект, который управляет независимым выполнением корутины в цикле событий.

Python 3.13
import asyncio
import time

async def worker_task(name, delay):
    print(f"Задача {name}: начинаю, буду ждать {delay} сек.")
    await asyncio.sleep(delay)
    print(f"Задача {name}: завершена.")
    return f"Результат от {name}"

async def main_concurrent():
    start_time = time.time()
    print(f"Запуск конкурентных задач в {time.strftime('%X')}")

    # Создаем задачи для конкурентного выполнения
    task1 = asyncio.create_task(worker_task("A", 2))
    task2 = asyncio.create_task(worker_task("B", 1))
    task3 = asyncio.create_task(worker_task("C", 3))

    # Ожидаем завершения всех задач конкурентно
    # (Более удобный способ дождаться нескольких задач - asyncio.gather())
    results = await asyncio.gather(task1, task2, task3)

    print(f"\nВсе задачи завершены за {time.time() - start_time:.2f} сек.")
    print(f"Результаты: {results}")

if __name__ == "__main__":
    asyncio.run(main_concurrent())

В этом примере worker_task("A", 2), worker_task("B", 1) и worker_task("C", 3) начнут выполняться конкурентно. Общее время выполнения будет близко к максимальной задержке (3 секунды), а не к сумме всех задержек.

asyncio.gather(*tasks) позволяет запустить несколько задач конкурентно и дождаться их всех, возвращая список результатов.

Объекты Future

Future — это специальный низкоуровневый объект, который представляет конечный результат асинхронной операции. Корутины обычно не работают с Future напрямую, но Task является подклассом Future.

  • Future можно ожидать (await future_obj).
  • У Future можно установить результат (future_obj.set_result()) или исключение (future_obj.set_exception()).

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

Простой асинхронный пример: "Hello, World"

Python 3.13
import asyncio

async def hello_world_async():
    print("Асинхронный Hello...")
    await asyncio.sleep(0.1) # Имитируем небольшую асинхронную операцию
    print("...World!")

if __name__ == "__main__":
    asyncio.run(hello_world_async())

Что дальше?

Мы рассмотрели самые основы asyncio: ключевые слова async/await, корутины, цикл событий, задачи и функцию asyncio.run(). Это база для написания эффективного I/O-bound кода.

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


Что является основной особенностью асинхронного программирования с asyncio?


Мы с вами на связи
Русский