[Перевод] Синхронный и асинхронный Python: в чем разница?

Python
[Перевод] Синхронный и асинхронный Python: в чем разница?

перевести
Sync vs. Async Python: What is the Difference?

Вы когда-нибудь слышали, как кто-то говорит, что асинхронный код Python быстрее, чем «обычный» (или синхронный) код Python? Как это возможно? В этой статье я попытаюсь объяснить, что такое асинхронность и чем она отличается от обычного кода Python.

Что означают синхронизация и асинхронность?

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

Термины «синхронизация» и «асинхронность» относятся к двум способам написания приложений, использующих параллелизм. Так называемые «синхронизирующие» серверы используют базовую поддержку операционной системы для потоков и процессов для достижения этого параллелизма. Ниже приведен рендеринг одновременного развертывания:

В данном случае у нас есть пять клиентов, все из которых отправляют запросы приложению. Общей точкой доступа для этого приложения является веб-сервер, который действует как балансировщик нагрузки, распределяя запросы на набор рабочих серверов, которые могут быть реализованы как процессы, потоки или их комбинация. Рабочие выполняют запросы, назначенные им балансировщиком нагрузки. Вы можете написать логику приложения в среде веб-приложений, такой как Flask или Django, и они живут в этих воркерах.

Этот тип решения идеально подходит для серверов с несколькими процессорами, потому что вы можете настроить количество рабочих процессов, кратное количеству процессоров, и с этой конфигурацией вы достигнете равномерного использования ядер, что невозможно с одним процессором. Процесс Python, потому чтоГлобальная блокировка интерпретатора (GIL)Вводятся некоторые ограничения.

Что касается недостатков, то приведенный выше рисунок ясно показывает основное ограничение этого метода. У нас 5 клиентов, но только 4 работника. Если эти 5 клиентов отправляют запросы одновременно, а балансировщик нагрузки может отправить только 1 запрос каждому рабочему процессу, запросы, которые не конкурируют с рабочим процессом, останутся в очереди, ожидая, пока рабочий процесс станет доступным. Так 4 из 5 клиентов получат ответ вовремя, но 1 из них придется ждать дольше. Ключом к хорошей работе сервера является выбор правильного количества рабочих процессов, чтобы предотвратить или свести к минимуму блокировку запросов с учетом ожидаемой нагрузки.

Настройки асинхронного сервера рисовать сложнее, но вот мои лучшие варианты:

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

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

Абстракция этой возможности приостанавливать и возобновлять выполнение асинхронных задач может быть трудной для понимания. Чтобы помочь вам применить это к вещам, которые вы, вероятно, уже знаете, учтите, что в Python один из способов сделать это — использоватьawaitилиyieldключевое слово, но это не единственный способ, как вы увидите позже.

Удивительно, как асинхронные приложения работают полностью в одном процессе и одном потоке. Конечно, этот тип параллелизма требует определенных правил, потому что выНе позволяйте задачам оставаться на ЦП слишком долго, иначе оставшиеся задачи умрут от голода.. Для асинхронной работы все задачи должны автоматически приостанавливаться и вовремя возвращать управление в цикл. Задачи, которые должно выполнять приложение, чтобы извлечь выгоду из асинхронного стиля.Обычно блокируется вводом-выводом и не требует большой загрузки процессора.. Веб-приложения часто хорошо подходят, особенно если им нужно обрабатывать множество клиентских запросов.

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

Два способа реализации асинхронности в Python

Я уверен, вы знаете, что для написания асинхронных приложений на Python вы можете использоватьасинхронный пакет, который основан на сопрограммах для реализации функций приостановки и возобновления, необходимых всем асинхронным приложениям. Ключевые словаyield, и обновленныйasyncа такжеawait, является основой, на которой asyncio строит асинхронные функции. Чтобы нарисовать полную картину, в экосистеме Python есть другие асинхронные решения на основе сопрограмм, такие какTrioи Curio. а такжеTwisted, который является старейшей структурой синергии, даже предшествующейasyncio.

Если вы заинтересованы в написании асинхронного веб-приложения, существует множество асинхронных фреймворков, основанных на сопрограммах, в том числеaiohttp,sanic,FastAPI и Tornado.

Чего многие люди не знают, так это того, что сопрограммы — это лишь один из двух способов в Python, которые можно использовать для написания асинхронного кода. Второй метод основан наgreenlet, который вы можете установить с помощью pip. Гринлеты похожи на сопрограммы в том, что они также позволяют функциям Python приостанавливать выполнение и возобновлять выполнение позже, но реализованы совершенно по-другому, а это означает, что асинхронная экосистема в Python делится на две широкие категории.

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

Итак, что в зелениasyncioА эквивалент ? Я знаю три асинхронных пакета на основе гринлетов:GeventEventlet иMeinheld, хотя последний больше похож на веб-сервер, чем на асинхронную библиотеку общего назначения. Они оба имеют собственную реализацию асинхронных циклов и предоставляют интересную функцию «monkey-patching», которая заменяет блокирующие функции в стандартной библиотеке Python, например те, которые выполняют сетевые и многопоточные операции, на эквивалентные неблокирующие версии, реализованные на гринлетах. Если у вас есть фрагмент синхронного кода, который вы хотите запускать асинхронно, есть большая вероятность, что эти пакеты позволят вам это сделать.

Вы будете удивлены этим. Насколько мне известно, единственный веб-фреймворк, явно поддерживающий гринлеты, — это Flask. Эта структура автоматически определяет, когда вы работаете на веб-сервере greenlet, и соответствующим образом настраивается без какой-либо настройки. При этом вы должны быть осторожны, чтобы не вызвать блокирующие функции, в противном случае используйте Monkey-patching, чтобы «исправить» эти блокирующие функции.

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

Асинхронность быстрее синхронизации?

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

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

переключатель контекста

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

Реализация цикла по умолчанию, предоставляемая asyncio, написана на Python и считается не очень эффективной.uvloopПакет предоставляет альтернативный цикл, который частично реализован в коде C для повышения производительности.Geventа такжеMeinheldИспользуемый цикл обработки событий также написан на языке C.EventletЦиклы написаны на Python.

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

Масштабируемость

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

Подумайте, что произойдет, если сервер синхронизации, показанный на изображении выше, получит 100 одновременных запросов. Сервер не может обрабатывать более 4 запросов одновременно, поэтому большинство из них некоторое время будут ждать в очереди, прежде чем можно будет назначить воркеров.

По сравнению с асинхронным сервером, который создает 100 задач одновременно (каждый из 4 асинхронных рабочих создает 25 задач при использовании гибридной модели). С асинхронным сервером все запросы могут начать обрабатываться без ожидания (хотя, честно говоря, могут быть и другие узкие места, которые замедляют работу, например ограничение на количество активных подключений к базе данных).

Если эти 100 задач интенсивно используют ЦП, синхронное и асинхронное решения будут иметь одинаковую производительность, поскольку ЦП работает с фиксированной скоростью, исполняемый код Python всегда имеет одинаковую скорость, а приложение выполняет одинаковую работу. Однако, если задача должна выполнять много операций ввода-вывода, всего с 4 одновременными запросами, сервер синхронизации может не достичь высокой загрузки ЦП. С другой стороны, асинхронный сервер определенно лучше загружает процессор, поскольку он выполняет все 100 запросов параллельно.

Вам может быть интересно, почему вы не можете запустить 100 рабочих процессов синхронизации, чтобы оба сервера имели одинаковый параллелизм.. Учтите, что у каждого воркера должен быть свой интерпретатор Python и все связанные с ним ресурсы, а также отдельная копия приложения со своими ресурсами. Размер сервера и приложения будет определять, сколько рабочих экземпляров может работать, но обычно это число не очень велико. С другой стороны, асинхронные задачи очень легкие и выполняются в контексте одного рабочего процесса, поэтому у них есть явное преимущество.

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

  • высокая нагрузка(Нет преимущества высокого параллелизма без высокой нагрузки)
  • Задача связана с вводом-выводом(Если задача привязана к процессору, параллелизм за пределами количества процессоров не помогает)
  • вы можете просмотретьСреднее количество запросов, обрабатываемых в единицу времени. Если вы посмотрите на время обработки одного запроса, вы не увидите большой разницы, а так как есть больше параллельных задач, конкурирующих за ЦП, асинхронность может быть даже немного медленнее.

Я надеюсь, что эта статья развеет некоторые недоразумения и недопонимания, связанные с асинхронным кодом. Я хочу, чтобы вы запомнили следующие два момента:

  • При высокой нагрузке асинхронные приложения будут работать только лучше, чем их синхронные эквиваленты.
  • Благодаря гринлетам вы можете извлечь выгоду из асинхронности, даже если вы пишете обычный код и используете традиционные фреймворки, такие как Flask или Django.

Если вы хотите более подробно взглянуть на то, как работают асинхронные системы, посмотрите мою презентацию PyCon.Asynchronous Python for the Complete Beginner.

关注PythonCN