
Асинхронное программирование на Python
- Понятие асинхронного программирования
- Синтаксис async/await и модуль asyncio
- Замечания по работе с asyncio
- Краткие итоги параграфа
- Вопросы и задания для самоконтроля

Понятие асинхронного программирования
Весь код, который мы использовали в наших примерах до сих пор, выполнялся последовательно, т.е. синхронно. При таком подходе каждая последующая операция блокируется интерпретатором до тех пор, пока не завершится выполнение предыдущей. Однако для решения некоторых практических задач с периодами длительного времени ожидания, например, при скачивании больших файлов, работе с базами данных или сетевыми запросами, такой подход не всегда себя оправдывает. Действительно, вместо того, чтобы простаивать, ожидая ответа от сервера, программа могла бы за это время успеть послать новый запрос или получить и обработать ответ на предыдущий. В программировании такой механизм может быть реализован, например, за счет асинхронного (непоследовательного) выполнения функций.
Асинхронное программирование – это концепция программирования, которая позволяет выполнять несколько задач одновременно в конкурентном режиме, не дожидаясь завершения каждой из них. При асинхронном программировании результат выполнения функций доступен не сразу, а через некоторое время в виде некоторых асинхронных (т.е. нарушающих последовательный порядок выполнения) вызовов.
При этом важно понимать, что сами функции не выполняются одновременно физически (например, параллельно в разных потоках или даже процессах, о которых мы будем говорить в следующем параграфе), а именно конкурируют в цикле событий за возможность продолжить работу, асинхронно переключаясь между собой в периоды ожидания завершения каких-либо длительных неблокирующих операций ввода-вывода или сетевых запросов. Как итог, значительно повышается отзывчивость такой программы, а также ее эффективность и производительность.
Синтаксис async/await и модуль asyncio в Python
Пусть нам требуется написать программу, которая будет принимать заказы от посетителей кафе и выводить на табло информацию о готовности заказа. Выполним для начала поставленную задачу в обычном синхронном режиме (см. пример №1).

# Нужен для получения меток времени.
import time
# Генерируем номера для заказов.
nums = (n for n in range(1, 1000))
# Принимает, готовит и сообщает о
# готовности заказа к выдаче.
def cook(time_to_prepare):
# Получаем уникальный номер заказа.
order = next(nums)
# Базовое сообщение на табло.
mess = 'Заказ №{}: {}'
# Выводим сообщение на табло.
print(mess.format(order, 'готовится...'))
# Период приготовления заказа.
time.sleep(time_to_prepare)
# Сообщаем о готовности заказа.
print(mess.format(order, 'готов!'))
# Вывод информации о заказах на табло.
def informer():
cook(3)
cook(1)
cook(2)
# Если запускается как основной модуль.
if __name__ == '__main__':
# Засекаем время начала операций.
start = time.time()
# Запускаем главную функцию программы.
informer()
# Время завершения операций.
stop = time.time()
# Время затраченное на все операции.
t = round(stop - start, 2)
print('Затраченное время: {} сек'.format(t))
Заказ №1: готовится...
Заказ №1: готов!
Заказ №2: готовится...
Заказ №2: готов!
Заказ №3: готовится...
Заказ №3: готов!
Затраченное время: 6.01 сек
Пример №1. Асинхронное программирование на Python (часть 1).
В нашем примере функция cook принимает от администратора время исполнения заказа, формирует его номер и выводит сообщение о заказе на экран. Время приготовления заказа мы симулируем с помощью функции sleep, используя при этом вместо минут секунды. Для симуляции процесса приема заказов и вывода информации о них на табло мы создали функцию informer и с ее помощью запустили на исполнение сразу три заказа. В итоге все заказы были выполнены последовательно один за другим, а общее время исполнения оказалось равным сумме временных затрат на выполнение каждого из заказов в отдельности (нажмите кнопку «Результат» и убедитесь в этом). Однако это явно не то, что мы бы хотели в итоге получить. Ведь в реальной жизни чаще всего заказы готовятся параллельно и не одним поваром. В итоге при передаче на кухню нескольких заказов и получении обратного ответа о времени приготовления каждого из них, все посетители должны быть обслужены в течении промежутка времени, отведенного на приготовление самого длительного заказа. В нашем случае это три минуты, но никак не шесть!
Одним из способов выхода из сложившейся ситуации является использование асинхронного подхода к решению задачи. Для этих целей в стандартной библиотеке Питона имеется модуль «asyncio», расположенный в разделе «Networking and Interprocess Communication». Давайте конвертируем нашу программу с использованием данного модуля и посмотрим, что из этого получится (см. пример №2).

# Нужен для получения меток времени.
import time, asyncio
# Генерируем номера для заказов.
nums = (n for n in range(1, 1000))
# Принимает, готовит и сообщает о
# готовности заказа к выдаче.
async def cook(time_to_prepare):
# Получаем уникальный номер заказа.
order = next(nums)
# Базовое сообщение на табло.
mess = 'Заказ №{}: {}'
# Выводим сообщение на табло.
print(mess.format(order, 'готовится...'))
# Период приготовления заказа.
await asyncio.sleep(time_to_prepare)
# Сообщаем о готовности заказа.
print(mess.format(order, 'готов!'))
# Вывод информации о заказах на табло.
async def informer():
# Создаем задачи для выполнения.
task_1 = asyncio.create_task(cook(3))
task_2 = asyncio.create_task(cook(1))
task_3 = asyncio.create_task(cook(2))
# Запускаем их.
await task_1
await task_2
await task_3
# Если запускается как основной модуль.
if __name__ == '__main__':
# Засекаем время начала операций.
start = time.time()
# Запускаем главную функцию программы.
asyncio.run(informer())
# Время завершения операций.
stop = time.time()
# Время затраченное на все операции.
t = round(stop - start, 2)
print('Затраченное время: {} сек'.format(t))
Заказ №1: готовится...
Заказ №2: готовится...
Заказ №3: готовится...
Заказ №2: готов!
Заказ №3: готов!
Заказ №1: готов!
Затраченное время: 3.03 сек
Пример №2. Асинхронное программирование на Python (часть 2).
Запустив данный скрипт (или для первоначального ознакомления нажав кнопку «Результат»), вы обнаружите, что время завершения всех заказов уменьшилось практически до трех секунд, т.е. до времени выполнения самого длительного из них. Кроме того, сообщения о начале исполнения заказов появились на табло в порядке запуска функций практически одновременно, а вот сообщения о готовности заказов – в порядке роста временных затрат на их приготовление. Это стало возможным за счет превращения наших функций в асинхронные. Чтобы понять, как мы это сделали, давайте рассмотрим общий алгоритм создания асинхронного кода в Python с помощью конструкции async/await и модуля asyncio стандартной библиотеки.
- Первым делом импортируется модуль asyncio.
- Функции, которые требуется запускать асинхронно, помечаются с помощью ключевого слова async в начале строки с ее определением (например, async def foo():). Вызов таких функций не запускает их на выполнение привычным нам способом, а возвращает специальные объекты, называемые корутинами (от англ. coroutine – сопрограмма). В отличие от обычных функций корутины могут начинаться, приостанавливаться и завершаться в произвольный момент времени.
- Далее создается некоторая основная асинхронная функция (например, async def main_loop():), тело которой используется для запуска рабочего цикла событий и организации асинхронного выполнения целевых корутин.
-
Внутри основной асинхронной функции конкурентный запуск корутин осуществляется двумя способами:
- корутины преобразуются в объекты задач с помощью метода asyncio.create_task(coro, *, name=None) (например, task_1 = asyncio.create_task(foo()) или asyncio.create_task(foo(), name='task_1')), после чего задачи запускаются на исполнение посредством использования ключевого слова await перед объектами целевых задач (например, await task_1);
- если задачи именовать не требуется, конкурентный запуск корутин может быть организован с помощью конструкции await asyncio.gather(*aws, loop=None, return_exceptions=False) за счет передачи методу требуемых объектов корутин (например, await asyncio.gather(foo_1(), foo_2(), foo_3())).
- В самом конце организуется запуск основной корутины. Делается это с помощью метода asyncio.run(coro, *, debug=False) (например, asyncio.run(main_loop()).
Еще раз внимательно пройдитесь по коду примера, но уже поэтапно, в свете представленного выше алгоритма. Ситуация конечно же далека от реальности, но симуляция вполне себе отражает суть асинхронного подхода в Python. Здесь не помешало бы сделать ряд оговорок, замечаний и предупреждений о наличии подводных камней, однако мы вернемся к этому чуть позже в последнем пункте нашего параграфа. Сейчас же давайте перепишем код примера еще раз, отметив основные этапы организации асинхронного кода в комментариях (см. пример №3).

# 1. Импортируем модуль asyncio.
import time, asyncio
nums = (n for n in range(1, 1000))
# 2. Объявляем функцию асинхронной.
async def cook(time_to_prepare):
order = next(nums)
mess = 'Заказ №{}: {}'
print(mess.format(order, 'готовится...'))
# Переключаемся на другие задачи.
await asyncio.sleep(time_to_prepare)
print(mess.format(order, 'готов!'))
# 3. Объявляем основную асинхронную функцию.
async def informer():
# 1-й способ асинхронного запуска корутин.
# 4.1. Создаем задачи для выполнения.
task_1 = asyncio.create_task(cook(3))
task_2 = asyncio.create_task(cook(1))
task_3 = asyncio.create_task(cook(2))
# 4.2. И запускаем их.
await task_1
await task_2
await task_3
# 2-й способ асинхронного запуска корутин.
# 4. Запускаем корутины в конкурентном режиме.
# await asyncio.gather(cook(3), cook(1), cook(2))
# 5. Запускаем основную корутину.
asyncio.run(informer())
Заказ №1: готовится...
Заказ №2: готовится...
Заказ №3: готовится...
Заказ №2: готов!
Заказ №3: готов!
Заказ №1: готов!
Пример №3. Асинхронное программирование на Python (часть 3).
Стоит отметить, что мы изложили лишь основу основ асинхронного подхода в программировании на Пайтон с целью получения первичного представления о нем. Сам же модуль asyncio предлагает значительно больше инструментов по организации и работе с асинхронным кодом. Поэтому в будущем, когда вы начнете пользоваться или работать над каким-нибудь реальным асинхронным приложением или даже библиотекой, вам наверняка придется заглянуть в официальную документацию и глубже разобраться в возможностях модуля. Сейчас же мы в качестве дополнительного примера приведем небольшой фрагмент асинхронного кода, использующего в своей работе aiohttp – довольно удобный асинхронный HTTP-клиент/сервер. Предлагаемый скрипт делает несколько сетевых запросов к разным сайтам и выводит время, затраченное как на каждый запрос в отдельности, так и на весь процесс в целом (см. пример №4).

# Импортируем модули.
import aiohttp, asyncio, time
# Асинхронный запрос страницы.
async def get_url(url):
# Измерим длительность одного запроса.
start = time.time()
# Асинхронный менеджер контекста.
async with aiohttp.ClientSession() as session:
# Получаем адрес страницы.
async with session.get(url) as response:
# Время, затраченное на его получение.
t = round(time.time() - start, 3)
# Выводим информацию.
info = "Запрос к {} занял {} секунд."
print(info.format(url, t))
# Основная асинхронная функция (цикл событий).
async def main():
# Стартовая метка времени.
start = time.time()
# Отправляем запросы в конкурентном режиме.
await asyncio.gather(
get_url("https://yandex.ru"),
get_url("https://google.com"),
get_url("https://okpython.net"),
get_url("https://html.okpython.net")
)
# Время, затраченное на его получение.
t = round(time.time() - start, 3)
# Выводим информацию.
info = "Все запросы заняли {} секунд."
print(info.format(t))
if __name__ == '__main__':
# Запускаем цикл событий.
asyncio.run(main())
Запрос к https://okpython.net занял 0.068 секунд.
Запрос к https://html.okpython.net занял 0.199 секунд.
Запрос к https://google.com занял 0.352 секунд.
Запрос к https://yandex.ru занял 0.426 секунд.
Все запросы заняли 0.426 секунд.
Пример №4. Асинхронное программирование на Python (часть 4).
Как видим, информация о запросах вывелась не последовательно, а в порядке завершения запросов. При этом общее время завершения операций оказалось приблизительно равным длительности самого долгого запроса.
Замечания по работе с asyncio
Начиная писать свой первый асинхронный код, программисты обычно сталкиваются с рядом досадных ошибок, которые приводят либо к появлению исключений, либо к тому, что код работает, но в обычном синхронном режиме. Поэтому сделаем несколько замечаний и поделимся парой полезных советов. Итак.
Для того, чтобы код начал работать в асинхронном режиме, недостаточно просто объявить функции асинхронными и запустить их обычным способом. Итогом будет создание объектов-корутин и получение исключения RuntimeWarning. Ошибкой TypeError завершится и попытка использования await для запуска обычной функции вместо передачи ей ожидаемого объекта, т.е. корутины. Во избежание таких оплошностей, основную корутину следует запускать с помощью метода asyncio.run(), а все остальные корутины, участвующие в конкурентном запуске, с помощью ключевого слова await и только внутри другой асинхронной функции (см. пример №5).

# Обязательно импортируем модуль.
import time, asyncio
# Объявляем функцию асинхронной.
async def sleep_func_1():
# Сообщим о начале сна.
print('func_1 засыпает на 1 секунду!')
# await time.sleep() приведет к TypeError, а
# просто time.sleep() заблокирует весь поток!
# Поэтому используем неблокирующую функцию.
await asyncio.sleep(1)
# Сообщаем о своем пробуждении.
print('func_1 проснулась!')
# Объявляем функцию асинхронной.
async def sleep_func_2():
# Сообщим о начале сна.
print('func_2 засыпает на 2 секунды!')
# await time.sleep() приведет к TypeError, а
# просто time.sleep() заблокирует весь поток!
# Поэтому используем неблокирующую функцию.
await asyncio.sleep(2)
# Сообщаем о своем пробуждении.
print('func_2 проснулась!')
# Основная асинхронная функция (цикл событий).
async def main_loop_func():
# Передаем именно корутины, а не сами функции.
task_1 = asyncio.create_task(sleep_func_2())
task_2 = asyncio.create_task(sleep_func_1())
# Запускаем их.
await task_1
await task_2
if __name__ == '__main__':
# Засекаем время начала операций.
start = time.time()
# Обязательно запускаем рабочий цикл.
asyncio.run(main_loop_func())
# Время завершения операций.
stop = time.time()
# Время затраченное на все операции.
t = round(stop - start, 2)
print('Затраченное время: {} сек'.format(t))
# Получим исключение RuntimeWarning.
# sleep_func_1()
# Вызываем только внутри асинхр. функций.
# await sleep_func_2()
func_2 засыпает на 2 секунды!
func_1 засыпает на 1 секунду!
func_1 проснулась!
func_2 проснулась!
Затраченное время: 2.0 сек
Пример №5. Асинхронное программирование на Python (часть 5).
Как вы уже наверняка заметили, почти во всех примерах для имитации режима ожидания, мы использовали не знакомую нам функцию time.sleep(), которая блокирует весь поток сразу, а ее асинхронный аналог asyncio.sleep(). Дело в том, что нет особого смысла в использовании внутри асинхронной функции лишь одних синхронных, т.к. в таком случае код будет выполняться самым обычным последовательным способом. При этом ошибок как таковых не возникнет, но опасность налицо: синхронные и блокирующие функции исправно работают и втихаря блокируют код, ничего не сообщая об этом вызвавшей их функции. Поэтому очень важно предусмотреть в теле асинхронной функции оптимальные точки переключения на другие конкурирующие задачи. Такими точками как раз и являются моменты входа функции в режим выполнения длительной (но неблокирующей!) операции.
Краткие итоги параграфа
- Асинхронное программирование – это концепция программирования, которая позволяет выполнять несколько задач одновременно в конкурентном режиме, не дожидаясь завершения каждой из них.
- Асинхронный подход организации кода хорошо подходит для решения некоторых практических задач с периодами длительного времени ожидания, например, при скачивании больших файлов, работе с базами данных или сетевыми запросами. Его использование повышает отзывчивость таких программ, а также их эффективность и производительность.
- В языке Пайтон также имеются инструменты для использования асинхронного программирования, основными из которых следует считать синтаксис async/await и модуль asyncio стандартной библиотеки.
- Функции, которые требуется запускать асинхронно, помечаются с помощью ключевого слова async в начале строки с ее определением. Вызов таких функций не запускает их на выполнение привычным нам способом, а возвращает специальные объекты, называемые корутинами. В отличие от обычных функций корутины могут начинаться, приостанавливаться и завершаться в произвольный момент времени.
- Для организации конкурентного запуска корутин их помещают в тело отдельной асинхронной функции. Там их преобразуют в объекты задач с помощью метода asyncio.create_task(coro, *, name=None) с последующим запуском посредством ключевого слова await или же используют конструкцию await asyncio.gather(*aws, loop=None, return_exceptions=False), если задачи именовать не требуется. При этом содержащая корутины внешняя функция должна быть запущена методом asyncio.run(coro, *, debug=False).
- Распространенные ошибки, допускаемые новичками: запуск асинхронных функций обычным вызовом или, наоборот, использование await для запуска синхронных функций; попытка использования await вне асинхронной функции; использование блокирующих функций внутри асинхронных; передача в соответствующие методы модуля asyncio объектов самих функций вместо возвращаемых ими при вызове корутин.
Вопросы и задания для самоконтроля
1. В чем заключается концепция асинхронного программирования? Показать решение.
Ответ. Концепция асинхронного программирования заключается в том, что программа не блокируется во время ожидания ответа от операций, которые могут занять много времени, а продолжает выполнять другие задачи. Это повышает отзывчивость, эффективность и производительность программы, особенно в сетевых и веб-приложениях.
2. Объясните назначение ключевых слов в конструкции async/await? Показать решение.
Ответ. Ключевое слово async предназначено для обозначения асинхронной функции. А ключевое слово await используется внутри асинхронной функции для приостановки ее выполнения до тех пор, пока не будет получен результат от другой асинхронной операции. await может применяться только к другим асинхронным объектам.
3. Что представляет из себя корутина? Показать решение.
Ответ. Корутина (она же сопрограмма) — это специальный объект, возвращаемый при вызове асинхронной функции, который может быть запущен, приостановлен или завершен в требуемый момент времени с помощью цикла событий или другим способом.
4. Назовите самый простой и рекомендуемый способ для запуска асинхронной программы (цикла событий)? Показать решение.
Ответ. Необходимо использовать функцию asyncio.run(coro), которая создает, запускает и закрывает цикл событий, а также запускает и ожидает завершение корутины coro, переданной в качестве аргумента.
5. Перечислите наиболее распространенные ошибки, допускаемые новичками при написании асинхронного кода. Показать решение.
Ответ. Запуск асинхронных функций обычным вызовом или, наоборот, использование await для запуска синхронных функций; попытка использования await вне асинхронной функции; использование блокирующих функций внутри асинхронных; передача в соответствующие методы модуля asyncio объектов самих функций вместо возвращаемых ими при вызове корутин.
Быстрый переход к другим страницам
- Дата и время в Python
- Асинхронное программирование на Python
- К содержанию учебника