Привет, Хабр! Меня зовут Николай Нагорный, я работаю в Росбанке над платформой 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)


  1. Superclip
    22.05.2023 10:42

    По поводу "Ограничений". Не обязательно искать асинхронные библиотеки. Можно убрать синхронные вызовы в ThreadPoolExecutor, особенно хорошо работает в случаях IO


    1. Nikolos193 Автор
      22.05.2023 10:42

      Я с вами полностью согласен что он очень хорош для IO.

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

      Спасибо за комментарий.Я добавлю ваше описание в статью.


      1. ri_gilfanov
        22.05.2023 10:42

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


        1. Tishka17
          22.05.2023 10:42

          А почему? Если у вас сложные вычисления, то у вас скорее всего не будет переключения контекста, наоборот должно работать "быстрее". Другой вопрос, что никакой конкурентности при этом не будет


          1. ri_gilfanov
            22.05.2023 10:42
            +1

            В моём представлении, "асинхронное программирование" -- синоним "конкурентной многозадачности". Поэтому я не совсем понимаю, что Вы имеете ввиду.

            Возьмём синтетический пример ограниченной ЦПУ (CPU bound) задачи. Например, вычисление 10.000 списков со списками квадратов целых чисел от 0 до 10.

            Пример реализации с синхронным (последовательным) выполнением:

            import timeit
            
            def foo():
                return [i ** i for i in range(10)]
            
            def bar():
                result = [foo() for i in range(10_000)]
                print(result)
            
            print(timeit.timeit(
                "bar()",
                setup="from __main__ import bar",
                number=1,
            ))

            Пример реализации потенциально поддерживающей асинхронное (конкурентное) выполнение (скажем, если бы работа корутины foo() была связана с независящими от ЦПУ задержками):

            import asyncio
            import timeit
            
            async def foo():
                return [i ** i for i in range(10)]
            
            async def bar():
                result = await asyncio.gather(*[foo() for i in range(10_000)])
                print(result)
            
            print(timeit.timeit(
                "asyncio.run(bar())",
                setup="from __main__ import bar; import asyncio",
                number=1,
            ))

            Инициализация цикла событий под капотом asyncio во втором примере не должна привносить больших накладных расходов на фоне 10.000 корутин.

            Запускаем каждый файл с примером 3 раза (используется CPython 3.10.8).

            Синхронный пример:

            0.04563898383639753
            0.04258333402685821
            0.048088460927829146

            Асинхронный пример:

            0.13237898400984704
            0.1389962760731578
            0.11636401992291212

            Неожиданно, конкурентный запуск 10.000 корутин в 2-3 раза превышает по времени 10.000 последовательных вызовов одной функции.

            А сколько памяти эти два процесса запрашивают у операционной системы?

            Синхронный пример:

            Maximum resident set size (kbytes): 13388
            Maximum resident set size (kbytes): 13380
            Maximum resident set size (kbytes): 13548

            Асинхронный пример:

            Maximum resident set size (kbytes): 31436
            Maximum resident set size (kbytes): 31224
            Maximum resident set size (kbytes): 31164

            Даже если импорт модуля asyncio самый затратный в стандартной библиотеке, на конкурентный запуск каждой корутины явно потребовалось больше 1 килобайта памяти. А выделение и освобождение памяти -- это тоже время. Особенно для кратковременных процессов или при скачках нагрузки.

            Конечно, можно было бы избавиться от asyncio.gather() и последовательно делать await корутины в цикле for... Вот только, по моему скромному мнению, последовательное выполнение -- это синоним синхронного выполнения.

            И если кто-то думает, что асинхронность -- это обмазать свои функции и вызовы ключевыми словами async/await и сунуть свой main() в asyncio.run(), значит у меня с этим человеком есть небольшое расхождение в терминах.

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


  1. ri_gilfanov
    22.05.2023 10:42
    +9

    В этом посте я подробно расскажу о важной фиче, которая появилась в Python 3.5 — асинхронности.

    Twisted -- 2002 год.
    Tornado -- 2009 год.
    Python 3.5 -- 2015 год.

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

    Поверим на слово, вдруг и правда полезен.

    Асинхронность — это парадигма программирования, которая позволяет выполнять несколько задач одновременно, не дожидаясь завершения каждой из них;

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

    важный инструмент для решения проблем с производительностью в веб-приложениях и серверных технологиях.

    Тоже скорее вреден. Слишком общая формулировка опускает различия между:

    • задачами упирающимися в ЦПУ

    • задачами упирающимися в дисковый ввод-вывод

    • задачами упирающимися в сетевой ввод-вывод

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

    Ой, всё...


    1. bekishev04
      22.05.2023 10:42
      +1

      Twisted -- 2002 год.
      Tornado -- 2009 год.
      Python 3.5 -- 2015 год.

      Ничего не могу сказать про Twisted, возможно там просто использовалось несколько потоков, а потом перешли на asyncio. А вообще идея asyncio была предоставлена Дэвидом Бизли в 2009 на Pycon. Изначально вместо ключевых слов async/await использовались итераторы


      1. ri_gilfanov
        22.05.2023 10:42

        Я лучше промолчу и Вам советую. Похоже именно за комментарий выше (оценки +6 -1) кто-то поправил мне карму с 0 на -1.


      1. cutwater
        22.05.2023 10:42

        В Twisted использовались реактор и коллбэки, да и сейчас используются. На asyncio он никогда не переходил.


    1. cutwater
      22.05.2023 10:42
      +1

      Eventlet -- 2008 год
      Gevent -- 2009 год


  1. bekishev04
    22.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