Привет, Хабр! Меня зовут Николай Нагорный, я работаю в Росбанке над платформой Advisors’ Axiom. В этом посте я подробно расскажу о важной фиче, которая появилась в Python 3.5 — асинхронности. Затрону основные концепции и инструменты, приведу примеры кода. Пост будет полезен новичкам для понимания основ асинхронности и, может, даже опытным разработчикам в поиске новых идей и подходов.
Начнем с определения. Асинхронность — это парадигма программирования, которая позволяет выполнять несколько задач одновременно, не дожидаясь завершения каждой из них; важный инструмент для решения проблем с производительностью в веб-приложениях и серверных технологиях. С момента появления асинхронности в Python сразу было доступно несколько способов реализации асинхронного кода. Чаще всего для асинхронных операций в Python используют библиотеки async/await и asyncio. Также асинхронность может быть использована для веб-приложений в сочетании с фреймворками, например с Django, Flask или Fast API.
Асинхронность в чистом Python
Один из способов реализации асинхронности в Python — декоратор @asyncio.coroutine. Вот функция-корутина, которая выполняет ожидание в течение некоторого времени:
import asyncio # импорт библиотеки
@asyncio.coroutine # декоратор
def my_coroutine(seconds):
   print ('Starting coroutine')
   yield from asyncio.sleep(seconds) # возвращаем результат
   print ('Finishing coroutine')
loop = asyncio.get_event_loop () # создаем объект
loop.run_until_complete(my_coroutine (2)) # запускаем
loop.close () # закрываемДругой, более предпочтительный способ появился в языке позднее — это асинхронные функции (Async Functions), которые вызываются через async. В примере ниже функция my_coroutine задана асинхронно, то есть программа не будет ждать завершения ее работы, а продолжит выполнение следующих команд:
import asyncio # импортируем библиотеку
async def my_async_function (seconds) : # создаем асинхронную функцию
   print ('Starting async function')
   await asyncio.sleep (seconds) # вызываем метод await для ожидания
   print ('Finishing async function')
loop = asyncio.get_event_loop () # создаем объект
loop.run_until_complete(my_async_function (2)) # запускаем
loop.close () # закрываемКроме того, в Python можно создавать асинхронные контексты с помощью ключевого слова async with. Это позволяет выполнять асинхронные операции внутри контекста, например открытие и закрытие файла:
import asyncio # импортируем библиотеку
async def read_file (filename): # создаем асинхронную функцию
   async with open(filename, 'r') as f: # открываем файл на чтение
      contents = await f. read () # читаем весь файл
      print (contents)Модуль asyncio с методами async functions, async with, контексты и корутины — выбор средств асинхронности в Python неплохой. Комбинируя их, можно создавать программы с высокой параллельностью и улучшать их производительность, а также избегать блокировок программы во время длительных операций. Помните, что асинхронный код сам по себе более сложен для понимания и реализации, поэтому используйте его с умом. И тогда перечисленные инструменты раскроют весь свой немаленький потенциал.
Асинхронность во фреймворках Python — Django, FastAPI, Flask
Теперь поговорим об использовании асинхронности с популярными фреймворками — Django, Flask и FastAPI. Ни один из них не имеет встроенной поддержки асинхронности, но ее можно добавить с помощью внешних библиотек.
Для реализации асинхронных вызовов FastAPI использует стандартный модуль asyncio и поддерживает асинхронные функции нативно, на уровне ядра. У Django и Flask такой поддержки нет, но они тоже умеют работать с асинхронными библиотеками asyncio или aiohttp. Так что разогнать свои веб-приложения с помощью асинхронности использование этих фреймворков не помешает. Вот пример реализации с библиотекой aiohttp в Flask:
import aiohttp # импортируем библиотеку для работы с асинхронным примером
from flask import Flask # импортируем библиотеку для работы с фреймворком
app = Flask(__name__) # создаем приложение
@app.route("/") # объявляем страницу
async def main(): # создаем асинхронную функцию
   async with aiohttp.ClientSession() as session: # открываем асинхронную клиентскую сессию
      async with session.get ("https://www.example.com") as response: # используя сессию делаем асинхронный запрос
         return response.text
if __name__ == Il “__main__”: # объявляем секцию main
   app.run() # запускаем фреймворк по с настройками по умолчаниюАсинхронность в веб-приложениях лучше всего раскрывается при работе с длительными операциями или вызовами API. Но учтите, что так вы подписываетесь на более сложную отладку и поддержку.
Вот еще пример асинхронных функций в FastAPI: функция main использует asyncio.sleep для задержки выполнения на 10 секунд. Это может пригодиться, например, для ожидания ответа от внешнего API:
from fastapi import FastAPI # импортируем библиотеку для работы с фреймворком
Aimport asyncio # импортируем библиотеку для работы с асинхронным примером
app = FastAPI() # создаем приложение
@app.get("/") # объявляем страницу
async def main(): #создаем асинхронную функцию
   await asyncio.sleep(10) # с помощью асинхронной библиотеки запускаем наш пример
   return {"message": "Hello World"}
if __name__ == "__main__": # объявляем секцию main
   uvicorn.run (app) # запускаем фреймворк по с настройками по умолчаниюЧто касается Django, то он также поддерживает асинхронное программирование с помощью внешних библиотек Django Channels.
from channels.generic.websocket import AsyncWebsocketConsumer #библиотека для работы с асинхронностью
§class MyConsumer (AsyncWebsocketConsumer):
   async def connect(self): # создаем асинхронный метод
      await self.accept() # ожидаем
      await asyncio.sleep (10) # выполняем действия
      await self.send(text_data="Hello World") # отправляем данные
      await self.close() # закрываемВ этом примере класс MyConsumer реализует асинхронное вебсокет-соединение через метод connect. Метод accept принимает соединение, а метод send отправляет сообщение клиенту. Для задержки отправки сообщения на 10 секунд используется функция asyncio.sleep. И в конце метод close закрывает соединение.
С помощью асинхронного программирования вы можете использовать все доступные ресурсы для обработки множества запросов одновременно, а не ставить их в очередь. Создаются асинхронные вьюхи в каждом фреймворке по-своему. В Django вы можете использовать декоратор async из библиотеки asyncio. Для Flask — декоратор asyncio.coroutine; Flask использует библиотеку gevent для управления асинхронными задачами. FastAPI имеет встроенную поддержку асинхронных функций, поэтому там можно использовать async def. Выбирайте фреймворк для работы с асинхронностью исходя из собственных задач и предпочтений. Я привел варианты выше, поскольку они точно предлагают удобный инструментарий для управления асинхронными задачами.
ORM — Object-Relational Mapping
ORM (Object-Relational Mapping) — это технология, которая позволяет связаться с базой данных, используя объекты Python, вместо того чтобы писать сырые SQL-запросы. Многие ORM для Python, такие как SQLAlchemy и Django ORM, поддерживают асинхронные версии. Библиотеки asyncio или asyncpg позволяют использовать асинхронные версии этих ORM в асинхронных приложениях.
С помощью ORM вы можете выполнять запросы к базе данных асинхронно, вместо того чтобы ждать ответа перед выполнением следующей задачи. Так вы можете увеличить производительность приложения и оптимизировать использование ресурсов. Пример асинхронного подключения к СУБД:
import asyncio # библиотека для работы с асинхронным кодом
import asyncpg # библиотека для подключения к субд
async def main(): ## создаем асинхронную функцию
   conn = await asyncpg.connect (user='user', password='password', database='database', host='host') # подключаем к субд
   # создание таблицы 
   await conn.execute('''
      CREATE TABLE IF NOT EXISTS test_table (
         id serial PRIMARY KEY,
         name text NOT NULL
      )
   ''')
   # вставка данных в таблицу
   await conn.execute('"
      INSERT INTO test_table (name)
      VALUES ($1)
   ''', 'John Doe')
   # выборка данных из таблицы
   result = await conn.fetch('''
      SELECT *
      FROM test_table
   ''') 
   print (result) # показываем результат
   await conn.close() # закрываем соединение
asyncio.run(main())В этом примере происходит соединение с базой данных, далее создание таблицы, вставка данных в таблицу и выборка данных. Все эти операции выполняются асинхронно, так что вы можете выполнять другие задачи одновременно с запросом к базе данных. Обработка запросов ускоряется и пользовательский опыт улучшается, так как во время ожидания ответа от базы данных приложение не проседает. Но еще раз напомню, что асинхронный код более сложен в реализации и отладке, поэтому оценивайте свои возможности и потребности проекта.
В нашей троице Django, Flask и FastAPI асинхронность в работе с базой данных можно реализовать через библиотеки asyncio и aiomysql. Они представляют собой асинхронные версии стандартных модулей для работы с базами данных, psycopg2 и mysql-python. Например в Django можно использовать asyncpg для работы PostgreSQL в асинхронном режиме. Соответственно, в Flask и FastAPI можно использовать aiomysql для MySQL.
Асинхронные ORM, такие как asyncio-orm или Tortoise-ORM, могут улучшить и производительность работы с базой данных в асинхронном режиме. В большинстве случаев асинхронный код позволяет использовать базу данных более эффективно за счет минимизации ожидания выполнения запросов. Покажу, как асинхронность может улучшить работу с базой данных, на примере с asyncio и aiomysql:
pip install aiomysql
import asyncio # библиотека для работы с асинхронным кодом
import aiomysql # библиотека для подключения к субд
async def main(): # создаем асинхронную функцию
   conn = await aiomysql.connect(host='localhost', user='user',
         password='password', db='dbname', charset='utf8mb4') # подключаем к субд
   async with conn.cursor() as cursor: # открываем подключение
         await cursor.execute ("SELECT * FROM table") # добавляем запрос
         result = await cursor.fetchall() # получаем все из таблицы print(result) # показываем запрос
asyncio.run(main ())Здесь мы создали простое приложение, которое подключается к базе данных и выполняет запрос. Используется асинхронный контекст-менеджер для управления соединением с базой данных, а затем выполняется SELECT-запрос и вывод результата.
Использование асинхронности в ORM может повысить производительность приложения, поскольку запросы к базе данных при этом выполняются в фоновом режиме, без блокировки потоков исполнения. Но помните о неминуемом усложнении кода и отладки. Django ORM и некоторые другие ORM поддерживают асинхронные запросы. Запросы эти не встроены в фундаментальный дизайн фреймворка и могут требовать дополнительных инструментов и настроек. С этой точки зрения проще использовать Flask с его asyncio.
Ограничения асинхронности
Не все базы данных поддерживают асинхронный режим работы — например реляционные базы данных, такие как PostgreSQL и MySQL. При использовании таких баз данных с асинхронным кодом вам могут потребоваться специальные библиотеки типа asyncpg.
Еще одно ограничение возникает при работе с транзакциями. Они обычно выполняются в блокирующем режиме, который заставляет все остальные запросы ждать, пока транзакция не будет завершена. Это может стать проблемой в асинхронных приложениях, так как они ожидают ответа в неблокирующем режиме. Здесь есть несколько решений: использование асинхронных ORM (asyncpg) или использование блокировок на уровне базы данных. Однако так вы будете вынуждены потерять в производительности и писать более сложный код.
Все три фреймворка, что мы рассматривали выше — FastAPI, Django и Flask — используют ORM (Object Relational Mapping) для управления базами данных. В ORM для работы с базами данных предусмотрены простые CRUD-операции (Create, Read, Update, Delete). В FastAPI и Flask ORM реализуют через сторонние библиотеки, такие как SQLAlchemy и Flask-SQLAlchemy. После этого с базами данных можно использовать и синхронные, и асинхронные операции (async for, async with, async/await). Django же с версии 3.0 поддерживает асинхронные операции с базой данных нативно, и вам тоже не придется ждать.
Заключение
Сегодня асинхронность очень важна для решения проблем с производительностью в веб-приложениях. В Python асинхронные функции можно организовать с помощью модуля asyncio. В фреймворках Django, Flask и FastAPI можно использовать асинхронные возможности для оптимизации работы с базами данных и выполнения сложных операций.
Следует помнить, что асинхронный стиль программирования усложнит код и отладку. И, конечно, потребует довольно глубокого понимания асинхронных принципов. Так что решение об использовании асинхронности должно быть осознанным и основываться на конкретных задачах.
Несмотря на это, асинхронность остается важным инструментом для улучшения производительности веб-приложений.
Комментарии (11)
 - ri_gilfanov22.05.2023 10:42+9- В этом посте я подробно расскажу о важной фиче, которая появилась в Python 3.5 — асинхронности. - Twisted -- 2002 год. 
 Tornado -- 2009 год.
 Python 3.5 -- 2015 год.- Пост будет полезен новичкам для понимания основ асинхронности и, может, даже опытным разработчикам в поиске новых идей и подходов. - Поверим на слово, вдруг и правда полезен. - Асинхронность — это парадигма программирования, которая позволяет выполнять несколько задач одновременно, не дожидаясь завершения каждой из них; - Скорее вреден. Слово "одновременно" может создать крайне кривое представление о конкурентной многозадачности, её принципиальных отличиях от параллельного выполнения и проблемах с блокирующими цикл событий вызовами. - важный инструмент для решения проблем с производительностью в веб-приложениях и серверных технологиях. - Тоже скорее вреден. Слишком общая формулировка опускает различия между: - задачами упирающимися в ЦПУ 
- задачами упирающимися в дисковый ввод-вывод 
- задачами упирающимися в сетевой ввод-вывод 
 - Комбинируя их, можно создавать программы с высокой параллельностью и улучшать их производительность, а также избегать блокировок программы во время длительных операций. - Ой, всё...  - bekishev0422.05.2023 10:42+1- Twisted -- 2002 год. 
 Tornado -- 2009 год.
 Python 3.5 -- 2015 год.- Ничего не могу сказать про Twisted, возможно там просто использовалось несколько потоков, а потом перешли на asyncio. А вообще идея asyncio была предоставлена Дэвидом Бизли в 2009 на Pycon. Изначально вместо ключевых слов async/await использовались итераторы  - ri_gilfanov22.05.2023 10:42- Я лучше промолчу и Вам советую. Похоже именно за комментарий выше (оценки +6 -1) кто-то поправил мне карму с 0 на -1. 
  - cutwater22.05.2023 10:42- В Twisted использовались реактор и коллбэки, да и сейчас используются. На asyncio он никогда не переходил. 
 
 
 - bekishev0422.05.2023 10:42+4- Может статья писалась ChatGPT потому что странно, что даже информация в самой статье противоречит друг другу - В этом посте я подробно расскажу о важной фиче, которая появилась в Python 3.5 — асинхронности - Неа, в python 3.5 завезли asyncio, а не асинхронность. - Есть разница между общим понятием асинхронности и asyncio. В общем понимании асинхронность это возможность выполнять несколько задач независимо. Это означает что асинхронность можно реализовать к примеру несколькими потоками. Только вот в случае потоков GIL будет переключаться между потоками и проверять надо ли ему работать или нет. asyncio решает эту проблему. В данной статье речь все же больше идет об asyncio - Модуль asyncio с методами async functions, async with, контексты и корутины — выбор средств асинхронности в Python неплохой. Комбинируя их, можно создавать программы с высокой параллельностью - asyncio это не про параллельность. В asyncio несколько задач не работают одновременно, пока одна задача ждет, другая может работать - Теперь поговорим об использовании асинхронности с популярными фреймворками — Django, Flask и FastAPI. Ни один из них не имеет встроенной поддержки асинхронности, но ее можно добавить с помощью внешних библиотек. - У FastAPI есть из коробки - С помощью ORM вы можете выполнять запросы к базе данных асинхронно, вместо того чтобы ждать ответа перед выполнением следующей задачи - Не обязательно с помощью ORM, ORM - это об удобстве работы и представлении данных в виде более сложных объектов, чем tuple/list/dict. - Так вы можете увеличить производительность приложения и оптимизировать использование ресурсов. Пример асинхронного подключения к СУБД: - Дальше идет пример. Я вот чего не понимаю, почему говорим про ORM, а используем сырые SQL-запросы? - Обработка запросов ускоряется - Не совсем так, скорее процессорное время тратится более оптимизировано. Если использовать синхронный запрос, он отработает скорее всего даже быстрее, если сразу начнет выполнятся. На практике запрос сначала попадает в очередь т.к. приложение способно одновременно обрабатывать к примеру только 4 запроса и начинает выполняться только тогда, когда приходит его очередь. 
 В случае использования async/awit тоже есть очередь, но в момент IO задач приложение может взять новый запрос, что невозможно в случае с синхронным выполнением.- Не все базы данных поддерживают асинхронный режим работы — например реляционные базы данных, такие как PostgreSQL и MySQL. - Эмм, чего? Весь ввод/вывод это IO задачи, как там работают бд нам на самом деле и не важно. - При использовании таких баз данных с асинхронным кодом вам могут потребоваться специальные библиотеки типа asyncpg. - Это всего лишь способ подключения, базу данных при этом asyncpg магическим образом не превращает в другую - С тем же успехом мы можем сказать что ОС работает синхронно, потому что для удаления файлов есть os.remove 
 Мне кажется автору стоит разобраться в БД/ORM/Асинхронности/Asyncio
 
           
 
Superclip
По поводу "Ограничений". Не обязательно искать асинхронные библиотеки. Можно убрать синхронные вызовы в ThreadPoolExecutor, особенно хорошо работает в случаях IO
Nikolos193 Автор
Я с вами полностью согласен что он очень хорош для IO.
Однако, следует учитывать, что ThreadPoolExecutor не решает проблемы масштабируемости и производительности, связанные с обработкой большого количества одновременных запросов или выполнением сложных вычислений. В таких случаях, асинхронные подходы и асинхронные библиотеки могут предоставить более эффективное решение.
Спасибо за комментарий.Я добавлю ваше описание в статью.
ri_gilfanov
Асинхронность по определению замедляет сложные вычисления. Например, для asyncio и trio в силу цикла событий и переключений контекста.
Tishka17
А почему? Если у вас сложные вычисления, то у вас скорее всего не будет переключения контекста, наоборот должно работать "быстрее". Другой вопрос, что никакой конкурентности при этом не будет
ri_gilfanov
В моём представлении, "асинхронное программирование" -- синоним "конкурентной многозадачности". Поэтому я не совсем понимаю, что Вы имеете ввиду.
Возьмём синтетический пример ограниченной ЦПУ (CPU bound) задачи. Например, вычисление 10.000 списков со списками квадратов целых чисел от 0 до 10.
Пример реализации с синхронным (последовательным) выполнением:
Пример реализации потенциально поддерживающей асинхронное (конкурентное) выполнение (скажем, если бы работа корутины
foo()была связана с независящими от ЦПУ задержками):Инициализация цикла событий под капотом
asyncioво втором примере не должна привносить больших накладных расходов на фоне 10.000 корутин.Запускаем каждый файл с примером 3 раза (используется CPython 3.10.8).
Синхронный пример:
Асинхронный пример:
Неожиданно, конкурентный запуск 10.000 корутин в 2-3 раза превышает по времени 10.000 последовательных вызовов одной функции.
А сколько памяти эти два процесса запрашивают у операционной системы?
Синхронный пример:
Асинхронный пример:
Даже если импорт модуля
asyncioсамый затратный в стандартной библиотеке, на конкурентный запуск каждой корутины явно потребовалось больше 1 килобайта памяти. А выделение и освобождение памяти -- это тоже время. Особенно для кратковременных процессов или при скачках нагрузки.Конечно, можно было бы избавиться от
asyncio.gather()и последовательно делатьawaitкорутины в циклеfor... Вот только, по моему скромному мнению, последовательное выполнение -- это синоним синхронного выполнения.И если кто-то думает, что асинхронность -- это обмазать свои функции и вызовы ключевыми словами
async/awaitи сунуть свойmain()вasyncio.run(), значит у меня с этим человеком есть небольшое расхождение в терминах.В общем, асинхронное программирование наиболее эффективно в немногих и довольно специфических задачах, вроде "быстро сделать запросы в 100 веб-сервисов" или "быстро реагировать на действия пользователя". Если подобных задач нет, оно привносит лишь накладные расходы, включая усложнение разработки, отладки и сопровождения.