Система блогов о разработке асинхронной платформы Python-FastAPI (3) Асинхронные функции

Python

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

адрес проекта

адрес блога

Асинхронное программирование — не новая концепция, но он не указал очень четких технических характеристик и маршрутов. Связанные понятия не очень ясны, и лишь немногие статьи могут объяснить это подробно.Блокирующий/неблокирующий, асинхронный/синхронный, параллельный/параллельный, распределенный, мультиплексирование ввода/вывода, сопрограммыРазличия и связи между этими понятиями. Эти концепции могут быть разработаны в курсах по ОС и распределенным системам для специалистов по информационным технологиям, но конкретный уровень реализации может редко затрагиваться. Что касается языка Python, я прочитал много статей, написанных специалистами отрасли и питонистами (или питонистами).

Xiaobai asyncio: принцип, исходный код для реализации (1);Конечно, название авторское скромное. Автор этой статьи описывает принцип разработки асинхронности Python, комбинируя стандартный исходный код asyncio, исходный код кадра стека функций и реализацию исходного кода контекста функции Python в CPython, а также написанную вручную упрощенную версию цикла событий и объекта asyncio-future.

Глубокое понимание асинхронного программирования Python (включено); эта статья была написана в 2017 году, до того, как asyncio стала стандартной библиотекой. В этой статье используется интерфейс epoll python и linux для пошаговой реализации однопоточного асинхронного ввода-вывода и, наконец, представлен цикл обработки событий asyncio, который доказывает его удобство. Автор планирует в следующей главе описать принцип asyncio, но пока не дождался следующей. Репозиторий, куда автор поместил код статьи, накопил десятки вопросов, требующих обновлений.

фундаментальный вопрос

Помните диаграмму последовательности, которую мы нарисовали в «Главе о коммуникации»? Можно использовать его для представления логики, выполняемой пользователем один раз, но в реальной реализации, можем ли мы написать такой код? Здесь есть два основных вопроса:

  • Проблема с одновременным доступом, как добиться одновременного доступа нескольких людей к вашему веб-процессу блога?

  • Как избежать блокировки ввода-вывода, чтобы в полной мере использовать временной интервал процессора?

Первая проблема знакома любому, кто занимался веб-разработкой, и у него есть множество решений, потому что это проблема, с которой необходимо столкнуться при разработке программного обеспечения:

  • уровень ОС, механизм мультиплексирования ввода-вывода, зрелый механизм epoll для Linux,nginxНа этом основано достижение параллелизма доступа.

  • Язык программирования использует многопоточность для решенияFlaskНапример, используйте собственные потоки для решения проблем безопасности потоков.

  • языки программирования решаются с помощью асинхронного программирования дляnodejsНапример,promise+ способ обратного звонка. питон этоasyncioПредставленная им асинхронная экосистема.

Второй вопрос на самом деле такой же, как и первый вопрос, просто замените объект на процессор.FrodoРешите первую проблему, используя что-то вроде цикла событий asyncio.uvloopцикл, он упаковывает его в возможностьASGIвеб-сервер протоколаuvicorn, он может начать несколькоASGIСтандартное письменное приложение со встроенным набором циклов событий для одновременного доступа.

uvicorn main:app --reload --host 0.0.0.0 --port 8001

Дело в томFrodoДля решения второй задачи это отражено в деталях программы.

Анализ проблемы: где блокировка ввода-вывода

Возьмем для примера коммуникационную логику CRUD в "Общение" Сначала отмечаем место блокировки IO, потом соответствуем ссылке в программировании, а потом думаем, как это решить в реализации.

На рисунке отмечены три типа сценариев ввода-вывода, причем некоторые из них являются последовательными требованиями, а некоторые — параллельными (могут быть параллельными) требованиями. Поясню отдельно:

  • первый сорт:Подключение и отключение сети, http является надежным протоколом передачи на основе tcp, а процесс установления соединения также является трудоемкой операцией ввода-вывода. Соединение с базой данных — это сетевое соединение или тип ссылки для чтения и записи файла сокета, что также требует много времени для io. Эти коды в основном в функции чекпойнта в сети, вFrodoизviewsПод содержанием.

  • Вторая категория:Асинхронная связь относится к процессу, в котором клиент отправляет запрос и ожидает, пока данные будут готовы к возврату.Эта часть времени ожидания фактически является операцией ввода-вывода данных бэкэнда, и ЦП не должен быть занят в это время. Эта часть кода находится вFrodoизmdoelsВниз.

  • Третья категория:Под асинхронностью данных понимается время, необходимое операциям базы данных для ожидания возврата данных. Эта часть времени также должна быть отдана процессору.

Многие из приведенных выше сценариев должны выполняться последовательно, например, установление соединения с базой данных --> операции с данными --> отключение. Существуют также некоторые сценарии (в основном сценарии, не связанные с согласованностью данных), которые можно распараллелить, например обновление и удаление кэша, поскольку база данных KV не предполагает одновременных отношений и может удаляться параллельно.

решение

Первая категория: длительное подключение

Соединение с базой данных и выходная синхронизация будут думать об использованииwithПул соединений ключевого слова, асинхронно, для того, чтобы этот процесс соединения "ждать" или передать право выполнения основной программе, необходимо использоватьasyncКлючевое слово оборачивает его и реализует метод асинхронного контекста.__aenter__, __aexit__.

import databases

class AioDataBase():
    async def __aenter__(self):
        db = databases.Database(DB_URL.replace('+pymysql', ''))
        await db.connect()
        self.db = db
        return db

    async def __aexit__(self, exc_type, exc, tb):
        if exc:
            traceback.print_exc()
        await self.db.disconnect()

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

Вторая категория: длительное общение

Является ли эта точка асинхронной или нет, определяет скорость отклика веб-приложения.Сама функция асинхронной контрольной точкиasync defсопрограммы ключевых слов, а затемuvloopрасписание. Требование к таким функциям — всегда использовать блокирующие операции.awaitПодождите, посмотрите пример:

@app.post('/auth')
async def login(req: Request, username: str=Form(...), password: str=Form(...)):
    user_auth: schemas.User = \
            ## 涉及到IO的函数需要等待
            await user.authenticate_user(username, password)
    if not user_auth:
        raise HTTPException(status_code=400, 
                            detail='Incorrect User Auth.')
    access_token_expires = timedelta(
        minutes=int(config.ACCESS_TOKEN_EXPIRE_MINUTES)
    )
    access_token = await user.create_access_token(
                        data={'sub': user_auth.name},
                        expires_delta=access_token_expires)
    return { ... }

async def authenticate_user(
        username: str, password: str) -> schemas.User:
    user = await User.async_first(name=username)
    user = schemas.UserAuth(**user)
    if not user: return False
    if not verify_password(password, user.password): return False
    return user

Возможно, вы заметили некоторые функции, такие какverify_passwordОн не ждет его, потому что он — вычислительная задача, и его нельзя ждать. Нам нужно только дождаться трудоемкой по логике операции io.

Третья категория: трудоемкие операции с данными

Это проявляется в асинхронностиORMВ конструкции метода,database + sqlalchemyПример реализации следующий:

@classmethod
async def asave(cls, *args, **kwargs):
    '''  update  '''
    table = cls.__table__
    id = kwargs.pop('id')
    async with AioDataBase() as db:
        query = table.update().\
                        where(table.c.id==id).\
                        values(**kwargs)
        ## 等待1: 执行sql语句
        rv = await db.execute(query=query)
    ## 等待2: 拿取数据构造对象
    obj = cls(**(await cls.async_first(id=id)))
    ## 等待3: 清除对象涉及的缓存
    await cls.__flush__(obj)
    return rv

Воспользуйтесь обновлениями данных в качестве примера, ожидание в ожидании. Синхронные рамки ORM, какpymysqlсуществуетdb.execute(...)Этот тип метода нельзя ждать, он напрямую блокируется, а метод асинхронной записи должен ждать своего результата.Преимущество в том, что право выполнения времени ожидания возвращается основной программе, чтобы она могла обрабатывать другие транзакции.

Параллельная реализация

Параллелизм в асинхронном режиме означает, что многие операции ввода-вывода не требуют согласованности данных и могут обрабатываться параллельно, например удаление несвязанных данных, запрос некоторых данных, обновление несвязанных данных и т. д., причем все эти операции могут выполняться параллельно. Эти параллелизмы также допускаются в асинхронном режиме с помощьюasycio.gather(*coros)Реализация метода, этот метод помещает все переданные сопрограммы в очередь цикла событий и выполняется одна за другой аналогичноcoro.send(None)операции, поскольку сопрограмма завершается немедленно, все сопрограммы могут быть разбужены и немедленно ожидать «одновременно» для достижения параллельного эффекта.

приемы, используемые в дизайне классов

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

Сериализация свойства @property класса

Сериализация объектов является обычным явлением, особенно когда вы хотите хранить объекты в кэше. Некоторые свойства объектов используются асинхронно@propertyЗавершенные, в отличие от других свойств, требуют специальных вызовов:

class Post(BaseModel):
    ...
    @property
    async def html_content(self):
        content = await self.content
        if not content:
            return ''
        return markdown(content)

этоpropertyНекоторые из них являются асинхронными и требуются каждый раз, когда используется это свойство.content = await post.html_contentБезasyncа такжеawaitДоступ к свойствам можно получить напрямуюcontent = post.html_content.

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

Пусть класс прикрепитpartialsатрибут, в котором хранится ожиданиеproperty, Для python для управления поведением класса (обратите внимание, что это поведение создания класса, а не поведение создания экземпляра) необходимо изменить свой метакласс, мы разрабатываем класс с именемPropertyHolderМетакласс, который позволяет его поведению управлять генерацией всех классов данных:

class PropertyHolder(type):
    """
    We want to make our class with som useful properties 
    and filter the private properties.
    """
    def __new__(cls, name, bases, attrs):
        new_cls = type.__new__(cls, name, bases, attrs)
        new_cls.property_fields = []

        for attr in list(attrs) + sum([list(vars(base))
                                       for base in bases], []):
            if attr.startswith('_') or attr in IGNORE_ATTRS:
                continue
            if isinstance(getattr(new_cls, attr), property):
                new_cls.property_fields.append(attr)
        return new_cls

Его функция состоит в том, чтобы отфильтровать то, что нам нужно.@property, выплачивается непосредственно классуpropertiesАтрибуты.

Далее идет изменениеBaseModelМетакласс генератора для:

@as_declarative()
class Base():
    __name__: str
    @declared_attr
    def __tablename__(cls) -> str:
        return cls.__name__.lower()

    @property
    def url(self):
        return f'/{self.__class__.__name__.lower()}/{self.id}/'

    @property
    def canonical_url(self):
        pass

class ModelMeta(Base.__class__, PropertyHolder):
    ...


class BaseModel(Base, metaclass=ModelMeta):
    ...

BaseЭто базовый класс ORM, и его собственный метакласс также был изменен (имеется в виду не тип). Если мы изменим его напрямую, наш тип данных потеряет функцию ORM. Лучшее из обоих миров - создать новый класс и наследуйте его одновременно.Baseа такжеPropertyHolder, что делает этот класс новым метаклассом миксина. (Это так запутанно, я не хочу здесь явления матрешки, я постепенно найду лучшее решение...).

трюки: как получить метакласс класса? перечислитьcls.__class__Получите метакласс, на котором он основан. Помните, что классы в Python сами по себе являются объектами. Его создание также контролируется.

О фастапи

хорошо,FrodoБыли представлены основные идеи дизайна первой версии.В предыдущем описании я редко упоминалfastapi, поскольку сам асинхронный веб не имеет ничего общего с фреймворком, этот набор контента заменяется наsanic,aiohttp,tornadoЧетноеDjangoВсе то же самое, но конкретные методы реализации разные, напримерDjangoАсинхронность основана на его собственном дизайнеchannelосуществленный.

ноfastapiЕсть и его особенности.Конструкторские идеи инклюзивны,а еще он много думает.В разработке настоятельно рекомендую использовать несколько мест:

  • схема данныхschemaдизайн, соответствиеpydanticПроверка типов в python делает динамический язык python более читабельным, легким для отладки и более стандартизированным в синтаксисе.Я считаю, что это будущая тенденция.

  • Depends, мы думали об инкапсуляции повторно используемой логики в классы, функции и декораторы, ноfastapiЧтоб городить о параметрах напрямую, меня удивляет, что он подменяет на параметры контекст, мультипараметры, параметры формы, параметры аутентификации и т.д.

  • Совместимость с синхронной записью, в том числеWSGI, используя синхронизированную технологию коллокации библиотекfastapiНикаких проблем, он допускает существование синхронных функций, причина в том, что он основан наASGIсчитать себяWSGIНадмножество , должно быть совместимо с обоими способами записи.

  • Благодаря преимуществам swagger-doc и серверной части вы можете успешно создать платформу отладки и документацию, которые смогут использовать и понимать как персонал клиентской, так и серверной части, не тратя время на изучение синтаксиса OpenAPI, экономя время и силы.

На этом три введения Фродо завершаются.Проекты, выполненные вне занятий и времени на исследования, неизбежно полны лазеек. Но первая версия была окончательно завершена после месяца боев. Будущая цель - море звезд Добавление новых языков, разделение мультисервисов и развертывание виртуализации требуют времени для тестирования, упорной работы~!