Зачем нужна асинхронность python
Перейти к содержимому

Зачем нужна асинхронность python

Асинхронное программирование в Python: краткий обзор

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

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

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

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

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

Асинхронное программирование в Python

Изначально в Python для решения задач асинхронного программирования использовались корутины, основанные на генераторах. Потом, в Python 3.4, появился модуль asyncio (иногда его название записывают как async IO ), в котором реализованы механизмы асинхронного программирования. В Python 3.5 появилась конструкция async/await.

Для того чтобы заниматься асинхронной разработкой на Python, нужно разобраться с парой понятий. Это — корутины (coroutine) и задачи (task).

Корутины

Обычно корутина — это асинхронная (async) функция. Корутина может быть и объектом, возвращённым из корутины-функции.

Если при объявлении функции указано то, что она является асинхронной, то вызывать её можно с использованием ключевого слова await :

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

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

Задачи

Задачи позволяют запускать корутины в цикле событий. Это упрощает управление выполнением нескольких корутин. Вот пример, в котором используются корутины и задачи. Обратите внимание на то, что сущности, объявленные с помощью конструкции async def — это корутины. Этот пример взят из официальной документации Python.

Функция say_after() имеет префикс async , в результате перед нами — корутина. Если немного отвлечься от этого примера, то можно сказать, что данную функцию можно вызвать так:

При таком подходе, однако, корутины вызываются последовательно и на их выполнение уходит около 3 секунд. В нашем же примере осуществляется их конкурентный запуск. Для каждой из них используется задача. В результате время выполнения всей программы составляет около 2 секунд. Обратите внимание на то, что для работы подобной программы недостаточно просто объявить функцию main() с ключевым словом async . В подобных ситуациях нужно пользоваться модулем asyncio .

Если запустить код примера, то на экран будет выведен текст, подобный следующему:

Обратите внимание на то, что отметки времени в первой и последней строках отличаются на 2 секунды. Если же запустить этот пример с последовательным вызовом корутин, то разница между отметками времени составит уже 3 секунды.

Пример

В этом примере производится нахождение количества операций, необходимых на вычисление суммы десяти элементов последовательности чисел. Вычисления производятся, начиная с конца последовательности. Рекурсивная функция начинает работу, получая число 10, потом вызывает сама себя с числами 9 и 8, складывая то, что будет возвращено. Подобное продолжается до завершения вычислений. В результате оказывается, например, что сумма последовательности чисел от 1 до 10 составляет 55. При этом наша функция весьма неэффективна, здесь используется конструкция time.sleep(0.1) .

Вот код функции:

Что произойдёт, если переписать этот код с использованием асинхронных механизмов и применить здесь конструкцию asyncio.gather , которая отвечает за выполнение двух задач и ожидает момента их завершения?

На самом деле, этот пример работает даже немного медленнее предыдущего, так как всё выполняется в одном потоке, а вызовы create_task , gather и прочие подобные создают дополнительную нагрузку на систему. Однако цель этого примера в том, чтобы продемонстрировать возможности по конкурентному запуску нескольких задач и по ожиданию их выполнения.

Итоги

Существуют ситуации, в которых использование задач и корутин оказывается весьма полезным Например, если в программе присутствует смесь операций ввода-вывода и вычислений, или если в одной и той же программе выполняются разные вычисления, можно решать эти задачи, запуская код в конкурентном, а не в последовательном режиме. Это способствует сокращению времени, необходимого программе на выполнение определённых действий. Однако это не позволяет, например, выполнять вычисления одновременно. Для организации подобных вычислений применяется мультипроцессинг. Это — отдельная большая тема.

Реализация асинхронности в Python с модулем asyncio

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

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

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

использовании асинхронности

На графике видно, что синхронный подход займет 45 секунд, в то время как при использовании асинхронности время выполнения можно сократить до 20 секунд.

Где асинхронность применяется в реальном мире?

Асинхронность больше всего подходит для таких сценариев:

  1. Программа выполняется слишком долго.
  2. Причина задержки — не вычисления, а ожидания ввода или вывода.
  3. Задачи, которые включают несколько одновременных операций ввода и вывода.
  • Парсеры,
  • Сетевые сервисы.

Разница в понятиях параллелизма, concurrency, поточности и асинхронности

Параллелизм — это выполнение нескольких операций за раз. Многопроцессорность — один из примеров. Отлично подходит для задач, нагружающих CPU.

Concurrency — более широкое понятие, которое описывает несколько задач, выполняющихся с перекрытием друг друга.

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

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

Составляющие асинхронного программирования

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

Сопрограммы

Сопрограммы (coroutine) — это обобщенные формы подпрограмм. Они используются для кооперативных задач и ведут себя как генераторы Python.

Для определения сопрограммы асинхронная функция использует ключевое слово await . При его использовании сопрограмма передает поток управления обратно в цикл событий (также известный как event loop).

Для запуска сопрограммы нужно запланировать его в цикле событий. После этого такие сопрограммы оборачиваются в задачи ( Tasks ) как объекты Future .

Пример сопрограммы

В коде ниже функция async_func вызывается из основной функции. Нужно добавить ключевое слово await при вызове синхронной функции. Функция async_func не будет делать ничего без await .

Python когда следует использовать async, а когда await?

А await ожидает завершения awaitable объекта и возвращает результат.

Их сложно перепутать: async — это часть утверждения (statement), await — унарный оператор, часть выражения (expression)

await , async for и async with должны использоваться только внутри асинхронной функции.

user avatar

Попытаюсь дополнить ответ @extrn.

async/await — это оптимизация, т.е. использовать их категорически не надо там, где все и так работает хорошо. Ведь это усложняет логику исполнения программы и тянет за собой много всего. Потому что если решились использовать асинхронную функциональность — будьте добры использовать ТОЛЬКО библиотеки, которые это поддерживают.

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

Асинхронный подход предполагает, что в само приложение задает специальные места, где такое переключение возможно. В Python в asyncio этими местами становятся вызовы с await .

Если решились оптимизировать, то в первую очередь стоит вспомнить что такое и чем различаются CPU-bound и IO-bound операции. Первые постоянно задействуют ресурсы процессора, все время что-то считают и вычисляют. Вторые большую часть времени ожидают операций ввода/вывода от файловой системы, сети или еще невесть чего.

Из-за GIL в Python (CPython) CPU-bound операции невозможно оптимизировать с использованием потоков, только вынесением в отдельный процесс. Совершенно то же самое ограничение накладывается на использование асинхронного подхода, потому что асинхронный код преимущественно одноядерный и если один исполнитель будет переключаться с одного процесса на другой, где его присутствие одинаково важно — никакого толку не будет (будет гораздо хуже).

Другое дело IO-bound задачи. Яркими примерами могут служить — чтение файлов, запрос в базу данных, даже time.sleep (который часто используется для имитации сложных IO-bound операций и есть специальный asyncio.sleep ).

Здесь функция execute не сильно нагрузит процессор, больше будет ждать ответа от базы данных. Поэтому процессору позволительно заняться в этот момент чем-нибудь другим. Но этот код синхронный, приходится ждать. Если нам надо построить очень много таких отчетов, то мы можем решиться оптимизировать это с использованием asyncio . Давайте попробуем.

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

Любая функция, которая использует await обязана объявляться как async — тут нет выбора. А await объявляет точку в программе, в которой мы можем переключиться на другую полезную работу, если такая есть, потому что ресурсы процессора здесь не нужны. Таким образом мы сможем успеть больше.

Оставляю за скобками инициализацию event-loop и прочие прелести запуска асинхронных программ.

Очень важно — сказал «А» ( async ), говори «B» ( await ), друг без друга они не существуют.

Крайне внятной я нашел следующую информацию:

Не знаю, есть ли в интернете перевод этого добра, но если нужен, я мог бы подготовить.

Напоследок, интересное утверждение: «используйте асинхронность, когда можете, а потоки — когда должны».

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *