Мысли об асинхронном и параллельном программировании на Python

Моей команде недавно была поручена задача по написанию клиента Python для взаимодействия с конкретным API WebSockets. Благодаря этому мы узнали новое, и цель этой статьи - поделиться своими успехами. Мы считаем, что всегда есть возможности для улучшения, поэтому не стесняйтесь комментировать и делиться своим опытом.

Когда мы говорим «за пределами пятистрочных образцов», мы имеем в виду, что мы нашли руководство, разговаривая с нашими коллегами и выполняя поиск в Интернете, но мы упустили ссылки, которые объединяют связанные технологии, советы по дизайну, библиотеки программирования и вопросы обеспечения качества. Я собираюсь заняться этим в следующих разделах.

Эта статья разделена на две части. Это вводная часть. Здесь я резюмирую некоторые важные аспекты WebSocket и их соответствие Python через websockets библиотеку. Во второй части я переключаю внимание на асинхронное и параллельное программирование, более глубоко исследуя asyncio встроенную библиотеку.

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

Рабочий объем

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

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

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

Замечание: в реальной задаче, которая послужила вдохновением для этой статьи, использование API WebSockets было единственным способом получить подробную информацию о документе. Мы использовали его для операций чтения; однако API также поддерживает создание и обслуживание программного документа - например, добавление / удаление компонентов и обновление данных презентации. В то же время он позволяет разработчикам интерфейсов создавать пользовательские интерфейсы, обеспечивающие пользователям живой опыт. Предложенного варианта использования для проверки документов достаточно, чтобы продемонстрировать концепции, которые будут рассмотрены в этой статье.

Произвольные события запускают рабочие нагрузки. Эти события, представленные E1 и E2, переносят идентификатор проверяемого документа. В этом сценарии документы состоят из более мелких компонентов, формально называемых графическими виджетами. Их проверка состоит из нескольких этапов:

  1. Отправьте сообщение WebSocket, чтобы получить общую информацию о документе.
  2. Отправьте n сообщений, чтобы получить подробную информацию о контейнере виджетов - их макет, положение и, что более важно, идентификатор вложенного виджета.
  3. Наконец, отправьте сообщения n, чтобы получить специфичные для виджета свойства, такие как тип (диаграмма, таблица или метка?), Цветовая палитра, размер шрифта и то, что на самом деле приносит пользу для текущего варианта использования: базовая база данных соединения, ссылки на электронные таблицы и операторы SQL.

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

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

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

Технический контекст

WebSockets

До присоединения к этому проекту мы уже работали с REST и GraphQL API. Мы также слышали о WebSockets, но не работали с ними.

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

- Википедия

Имея в виду это элементарное описание, пришло время искать библиотеку Python для подключения к API. Мы использовали библиотеку requests для работы с REST и GraphQL, но она не работает с WebSockets.

Библиотеки Python

Их несколько, и мы выбрали aaugustin / websockets, потому что:

  • Пользоваться им довольно просто.
  • Он построен на основе asyncio, стандартной среды асинхронного ввода-вывода Python.
  • Его репозиторий GitHub имеет больше звезд, чем конкуренты, такие как websocket-client / websocket-client, что намекает на предпочтения сообщества.

Имейте в виду, что для библиотеки websockets требуется Python ≥ 3.6.1. Для нас это не проблема, потому что у проекта есть другие зависимости, для которых требуется такая же дополнительная версия. И наоборот, websocket-client работает со старыми версиями - согласно их документации, он протестирован на Python 2.7 и Python 3.4+. Похоже, поддержка Python 3 все еще продолжается.

Спецификации обмена данными

Сообщения WebSocket могут нести текстовые данные, двоичные данные и контрольные фреймы.

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

- Протокол WebSocket

Я не буду вдаваться в подробности контрольных фреймов, потому что они внутренне обрабатываются библиотекой websockets. С другой стороны, сервер и клиент должны согласовать стандарты, которые они будут использовать для обмена данными. Это цель этого раздела.

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

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

Затем, чтобы запросить общую информацию о документе (шаг 1 рабочего процесса проверки), клиенту необходимо выполнить исходящее сообщение следующим образом:

Чтобы запросить информацию о контейнере виджета (шаг 2):

И так далее. Надеюсь, я достаточно ясно объяснил шаблон входящего сообщения сервера на примерах, а не на формальной схеме!

Вы, наверное, заметили, что я не использовал запрос / ответ в области связи WebSockets. Это было сделано намеренно, чтобы избежать путаницы с запросами / ответами REST или GraphQL. Поскольку WebSockets предоставляют дуплексные каналы связи, которые служат нескольким целям, а сообщения могут приходить и уходить в различных обстоятельствах, имеет смысл называть их исходящими / входящими сообщениями или даже сообщениями / ответами.

Как в реальном варианте использования, который я использую в качестве справки, так и в фиктивном, клиент ожидает ответа на каждое отправленное сообщение с данными. Вот почему мы включаем поле id в каждое сообщение. Сам протокол WebSocket не гарантирует, что на каждое сообщение будет ответ. Время от времени ответы не будут приходить в том же порядке, в котором сервер получил соответствующие сообщения. Таким образом, включение id в сообщение - это еще одно соглашение между сервером и клиентами о реализации простого механизма отслеживания сообщений / ответов.

Тем не менее, на сообщение GetDocument с id = 1 можно ответить следующим образом:

Прежде чем двигаться дальше, важно понять:

  • Мы стремимся подключиться к серверу WebSocket, который указывает JSON в качестве формата обмена данными.
  • Соединения WebSocket обычно используют один URI, например wss://documents.example.com - строки запроса и заголовки запросов действительны при установлении соединений.
  • Все параметры связи, включая методы и входные аргументы, отправляются через тело сообщения.

Обсуждение дешево - Покажи мне код

Пришло время познакомиться с некоторыми вещами о Python! Полный пример доступен на GitHub.

Начнем с websockets использования библиотеки. Как уже упоминалось, эта библиотека упрощает создание экземпляра клиента, а также отправку и получение сообщений по дуплексным каналам.

В приведенном ниже фрагменте кода показано, как его использовать:

Подобные утверждения вы найдете в полном примере.

Обратите внимание, что сообщения и ответы обрабатываются отдельно. Если вы работали с REST или GraphQL API, вы знаете, что это другое поведение. REST и GraphQL используют HTTP, поэтому мы получаем ответ сразу после отправки запроса. При работе с WebSockets можно отправлять n сообщений, подождать некоторое время, а затем получать ответы - или нет. Эта картинка иллюстрирует то, что я имею в виду:

Что дальше?

Параллельные задачи помогают нам обрабатывать сообщения и ответы в асинхронных каналах связи WebSocket, хотя здесь есть компромисс сложности. Итак, они подробно освещены вместе с asyncio во второй части статьи.

Спасибо, что дочитали до этого места!

использованная литература