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

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

Зачем нужно асинхронное программирование?

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

Асинхронное программирование решает эту проблему: вместо того чтобы блокировать выполнение программы при ожидании, оно позволяет продолжать выполнение других задач, пока не завершится ожидаемая операция. Это значительно увеличивает производительность и эффективность программы, особенно в сетевых приложениях.

 

История асинхронного программирования в Python

Асинхронное программирование стало важной частью экосистемы Python с введением библиотеки asyncio в Python 3.4, а позже синтаксических конструкций async и await в Python 3.5. Эти нововведения облегчили написание асинхронного кода, сделав его более удобным и читаемым.

До asyncio в Python для выполнения параллельных задач использовались модули threading и multiprocessing, но они требовали создания потоков или процессов, что было сложнее в реализации и могло занимать больше системных ресурсов.

 

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

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

В Python асинхронное программирование часто использует следующие ключевые элементы:

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

 

Основные конструкции: async и await

Для работы с асинхронным программированием в Python используются две ключевые конструкции: async и await.

  • async def: объявляет асинхронную функцию (коррутину), которая возвращает объект коррутины. Такие функции могут использовать ключевое слово await для ожидания других асинхронных функций.
  • await: приостанавливает выполнение текущей коррутины до тех пор, пока не завершится другая коррутина или асинхронная операция.

Пример использования async и await:

import asyncio

# Объявляем асинхронную функцию
async def greet():
    print("Hello...")
    await asyncio.sleep(2)  # Приостановим выполнение на 2 секунды
    print("...world!")

# Запускаем асинхронную функцию через цикл событий
asyncio.run(greet())

Объяснение:

  1. Мы объявили асинхронную функцию greet с использованием async def.
  2. Внутри функции использована функция asyncio.sleep(2), которая асинхронно ждет 2 секунды.
  3. await указывает, что мы ждем завершения выполнения операции asyncio.sleep, при этом программа может выполнять другие задачи.
  4. asyncio.run используется для запуска асинхронной функции через цикл событий.

 

Пример: Асинхронные сетевые запросы

Рассмотрим более практичный пример, где асинхронное программирование значительно ускоряет выполнение. Пусть у нас есть задача отправить несколько HTTP-запросов. Синхронный код выполнял бы каждый запрос последовательно, блокируя выполнение программы на время ожидания ответа. Асинхронный код позволяет отправить все запросы сразу и ждать их завершения одновременно.

Для этого примера используем библиотеку aiohttp:

import aiohttp
import asyncio

# Асинхронная функция для выполнения HTTP-запроса
async def fetch_url(session, url):
    async with session.get(url) as response:
        return await response.text()

# Главная асинхронная функция, выполняющая несколько запросов
async def fetch_all(urls):
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_url(session, url) for url in urls]
        return await asyncio.gather(*tasks)

# Список URL для загрузки
urls = ['https://example.com', 'https://httpbin.org']

# Запуск цикла событий
results = asyncio.run(fetch_all(urls))

# Вывод результатов
for result in results:
    print(result)

Объяснение:

  1. Мы используем библиотеку aiohttp, которая поддерживает асинхронные HTTP-запросы.
  2. Функция fetch_url выполняет HTTP-запрос к URL и возвращает содержимое страницы. Мы используем await для ожидания завершения запроса.
  3. В функции fetch_all создается несколько задач для выполнения запросов одновременно, а asyncio.gather позволяет собрать все результаты сразу.
  4. Асинхронное выполнение запросов позволяет сократить общее время ожидания.

 

Синхронные и асинхронные исключения

Асинхронные функции могут выбрасывать исключения, как и обычные синхронные функции. Они обрабатываются привычным способом через try/except.

Пример обработки исключений в асинхронном коде:

import asyncio

# Асинхронная функция, которая выбрасывает исключение
async def might_fail():
    await asyncio.sleep(1)
    raise ValueError("Что-то пошло не так!")

# Главная функция
async def main():
    try:
        await might_fail()
    except ValueError as e:
        print(f"Ошибка: {e}")

# Запуск цикла событий
asyncio.run(main())

Объяснение:

  1. Функция might_fail ждет 1 секунду, а затем выбрасывает исключение ValueError.
  2. В функции main исключение перехватывается через try/except, и сообщение об ошибке выводится на экран.

 

Сложные задачи и их решение

Теперь давайте рассмотрим несколько более сложных сценариев использования асинхронного программирования.


Задача 1: Асинхронное ожидание нескольких операций

Представьте, что нам нужно одновременно обрабатывать несколько разных задач, каждая из которых занимает разное время. Например, одна задача делает HTTP-запрос, другая читает файл, а третья просто ждет некоторое время.

Решение:

import asyncio

# Асинхронная функция для чтения файла
async def read_file():
    await asyncio.sleep(2)  # Симуляция чтения файла
    print("Файл прочитан")

# Асинхронная функция для выполнения HTTP-запроса
async def fetch_data():
    await asyncio.sleep(3)  # Симуляция запроса
    print("Данные получены")

# Асинхронная функция, которая просто ждет
async def wait_for_something():
    await asyncio.sleep(1)
    print("Задача завершена")

# Главная функция, которая запускает все задачи одновременно
async def main():
    tasks = [
        read_file(),
        fetch_data(),
        wait_for_something()
    ]
    await asyncio.gather(*tasks)

# Запуск цикла событий
asyncio.run(main())

Объяснение:

  1. Мы создали три асинхронные функции:
    • read_file(), которая симулирует чтение файла с помощью asyncio.sleep(2).
    • fetch_data(), которая симулирует выполнение HTTP-запроса с помощью asyncio.sleep(3).
    • wait_for_something(), которая просто ждет 1 секунду перед завершением.
  2. В функции main() мы собрали все задачи в список и одновременно запустили их с помощью asyncio.gather.
  3. Программа завершает выполнение всех задач параллельно, несмотря на разное время выполнения каждой задачи. Это позволяет эффективно использовать время ожидания.


Задача 2: Периодическое выполнение задач

Иногда требуется периодически выполнять определенные задачи, например, каждые 5 секунд отправлять запрос к серверу. Асинхронное программирование позволяет это сделать без блокировки основной программы.

Решение:

import asyncio

# Асинхронная функция, которая выполняется каждые 5 секунд
async def periodic_task():
    while True:
        print("Отправка запроса...")
        await asyncio.sleep(5)  # Ждем 5 секунд перед следующей отправкой

# Главная функция
async def main():
    task = asyncio.create_task(periodic_task())  # Запускаем периодическую задачу
    await asyncio.sleep(15)  # Ждем 15 секунд
    task.cancel()  # Прерываем выполнение периодической задачи

# Запуск цикла событий
asyncio.run(main())

Объяснение:

  1. Функция periodic_task() выполняет бесконечный цикл, где каждую итерацию она ждет 5 секунд, а затем выполняет свою задачу (в данном случае просто вывод сообщения).
  2. В функции main() мы создаем задачу periodic_task() и ждем 15 секунд перед ее отменой с помощью task.cancel().
  3. Программа работает параллельно с периодической задачей, не блокируя основной поток, что позволяет в дальнейшем дополнять программу другими асинхронными операциями.

 

Асинхронные тайм-ауты

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

Для задания тайм-аутов в Python используется функция asyncio.wait_for.

Пример:

import asyncio

# Асинхронная функция, которая выполняется с задержкой
async def slow_operation():
    await asyncio.sleep(10)  # Симулируем долгую операцию
    return "Операция завершена"

# Главная функция
async def main():
    try:
        # Попытаемся завершить операцию за 5 секунд
        result = await asyncio.wait_for(slow_operation(), timeout=5)
        print(result)
    except asyncio.TimeoutError:
        print("Операция заняла слишком много времени и была прервана")

# Запуск цикла событий
asyncio.run(main())

Объяснение:

  1. В примере операция slow_operation() длится 10 секунд.
  2. В функции main() с помощью asyncio.wait_for() мы задаем тайм-аут в 5 секунд для выполнения операции. Если она не успеет завершиться за это время, будет выброшено исключение asyncio.TimeoutError, которое мы обрабатываем.

 

Когда использовать асинхронное программирование?

Асинхронное программирование полезно в следующих сценариях:

  • Сетевые приложения: При работе с HTTP-запросами, базами данных или веб-сокетами. Например, веб-серверы, клиенты или чаты, где требуется обслуживать множество пользователей одновременно.
  • Чтение/запись файлов: Когда необходимо параллельно обрабатывать файлы или данные, но сами операции ввода/вывода могут занять значительное время.
  • Таймеры и задачи с задержками: Например, периодические задачи, уведомления или тайм-ауты для операций.
  • Интерактивные приложения: Программы, которые должны обрабатывать действия пользователя или события в реальном времени без задержек (например, игры или графические интерфейсы).

 

Вот таблица с ключевыми моментами из лекции об асинхронном программировании:

ТемаОписание
Зачем нужно асинхронное программирование?Увеличивает производительность, позволяя программе выполнять другие задачи, пока ожидаются внешние операции (например, сетевые запросы), вместо блокировки программы.
История асинхронного программирования в PythonВведено в Python с библиотекой asyncio (Python 3.4), синтаксические конструкции async и await добавлены в Python 3.5 для упрощения написания асинхронного кода.
Цикл событийОсновной механизм, который управляет выполнением задач, переключаясь между ними по мере необходимости, без блокировки выполнения программы.
КоррутиныАсинхронные функции, которые могут приостанавливать свое выполнение и продолжать его позже.
Ключевые элементы- Коррутины (асинхронные функции) 
- Цикл событий (управляет задачами) 
- Асинхронные операции (не блокируют программу)
async и await- async def: объявляет асинхронную функцию. 
- await: приостанавливает выполнение коррутины до завершения асинхронной операции.
Асинхронные HTTP-запросыАсинхронное программирование позволяет параллельно выполнять несколько сетевых запросов, сокращая время ожидания и повышая эффективность.
Исключения в асинхронных функцияхОбрабатываются стандартным механизмом через try/except, как и в синхронных функциях.
Асинхронное ожидание нескольких операцийПозволяет одновременно выполнять несколько задач, каждая из которых занимает разное время.
Периодические задачиАсинхронное программирование позволяет выполнять задачи через определенные интервалы времени, не блокируя основную программу.
Асинхронные тайм-аутыasyncio.wait_for задает тайм-аут для завершения асинхронной операции. Если она не завершится вовремя, выбрасывается исключение TimeoutError.
Когда использовать асинхронное программирование?- Сетевые приложения 
- Чтение/запись файлов 
- Таймеры и задачи с задержками 
- Интерактивные приложения

 

Асинхронное программирование позволяет писать более эффективные и масштабируемые приложения, минимизируя блокировку операций, таких как ожидание сетевых запросов или работы с файлами. Благодаря новым возможностям Python, таким как async и await, написание асинхронного кода стало проще и понятнее.

Хотя асинхронное программирование не заменяет другие методы параллельности, такие как многопоточность или многопроцессорность, оно является отличным инструментом для решения задач, связанных с ожиданием ввода-вывода.

Перейти к следующему шагу

Комментарии