Асинхронное программирование в 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
Асинхронное программирование на Python становится все более популярным. Для этих целей существует множество различных библиотек. Самая популярная из них — Asyncio, которая является стандартной библиотекой Python 3.4. Из этой статьи вы узнаете, что такое асинхронное программирование и чем отличаются различные библиотеки, реализующие асинхронность в Python.
По очереди
В каждой программе строки кода выполняются поочередно. Например, если у вас есть строка кода, которая запрашивает что-либо с сервера, то это означает, что ваша программа не делает ничего во время ожидания ответа. В некоторых случаях это допустимо, но во многих — нет. Одним из решений этой проблемы являются потоки (threads).
Потоки дают возможность вашей программе выполнять ряд задач одновременно. Конечно, у потоков есть ряд недостатков. Многопоточные программы являются более сложными и, как правило, более подвержены ошибкам. Они включают в себя такие проблемы: состояние гонки (race condition), взаимная (deadlock) и активная (livelock) блокировка, исчерпание ресурсов (resource starvation).
Переключение контекста
Хотя асинхронное программирование и позволяет обойти проблемные места потоков, оно было разработано для совершенно другой цели — для переключения контекста процессора. Когда у вас есть несколько потоков, каждое ядро процессора может запускать только один поток за раз. Для того, чтобы все потоки/процессы могли совместно использовать ресурсы, процессор очень часто переключает контекст. Чтобы упростить работу, процессор с произвольной периодичностью сохраняет всю контекстную информацию потока и переключается на другой поток.
Асинхронное программирование — это потоковая обработка программного обеспечения / пользовательского пространства, где приложение, а не процессор, управляет потоками и переключением контекста. В асинхронном программировании контекст переключается только в заданных точках переключения, а не с периодичностью, определенной CPU.
Эффективный секретарь
Теперь давайте рассмотрим эти понятия на примерах из жизни. Представьте секретаря, который настолько эффективен, что не тратит время впустую. У него есть пять заданий, которые он выполняет одновременно: отвечает на телефонные звонки, принимает посетителей, пытается забронировать билеты на самолет, контролирует графики встреч и заполняет документы. Теперь представьте, что такие задачи, как контроль графиков встреч, прием телефонных звонков и посетителей, повторяются не часто и распределены во времени. Таким образом, большую часть времени секретарь разговаривает по телефону с авиакомпанией, заполняя при этом документы. Это легко представить. Когда поступит телефонный звонок, он поставит разговор с авиакомпанией на паузу, ответит на звонок, а затем вернется к разговору с авиакомпанией. В любое время, когда новая задача потребует внимания секретаря, заполнение документов будет отложено, поскольку оно не критично. Секретарь, выполняющий несколько задач одновременно, переключает контекст в нужное ему время. Он асинхронный.
Потоки — это пять секретарей, у каждого из которых по одной задаче, но только одному из них разрешено работать в определенный момент времени. Для того, чтобы секретари работали в потоковом режиме, необходимо устройство, которое контролирует их работу, но ничего не понимает в самих задачах. Поскольку устройство не понимает характер задач, оно постоянно переключалось бы между пятью секретарями, даже если трое из них сидят, ничего не делая. Около 57% (чуть меньше, чем 3/5) переключения контекста были бы напрасны. Несмотря на то, что переключение контекста процессора является невероятно быстрым, оно все равно отнимает время и ресурсы процессора.
Зеленые потоки
Зеленые потоки (green threads) являются примитивным уровнем асинхронного программирования. Зеленый поток — это обычный поток, за исключением того, что переключения между потоками производятся в коде приложения, а не в процессоре. Gevent — известная Python-библиотека для использования зеленых потоков. Gevent — это зеленые потоки и сетевая библиотека неблокирующего ввода-вывода Eventlet. Gevent.monkey изменяет поведение стандартных библиотек Python таким образом, что они позволяют выполнять неблокирующие операции ввода-вывода. Вот пример использования Gevent для одновременного обращения к нескольким URL-адресам:
Как видите, API-интерфейс Gevent выглядит так же, как и потоки. Однако за кадром он использует сопрограммы (coroutines), а не потоки, и запускает их в цикле событий (event loop) для постановки в очередь. Это значит, что вы получаете преимущества потоков, без понимания сопрограмм, но вы не избавляетесь от проблем, связанных с потоками. Gevent — хорошая библиотека, но только для тех, кто понимает, как работают потоки.
Давайте рассмотрим некоторые аспекты асинхронного программирования. Один из таких аспектов — это цикл событий. Цикл событий — это очередь событий/заданий и цикл, который вытягивает задания из очереди и запускает их. Эти задания называются сопрограммами. Они представляют собой небольшой набор команд, содержащих, помимо прочего, инструкции о том, какие события при необходимости нужно возвращать в очередь.
Функция обратного вызова (callback)
В Python много библиотек для асинхронного программирования, наиболее популярными являются Tornado, Asyncio и Gevent. Давайте посмотрим, как работает Tornado. Он использует стиль обратного вызова (callbacks) для асинхронного сетевого ввода-вывода. Обратный вызов — это функция, которая означает: «Как только это будет сделано, выполните эту функцию». Другими словами, вы звоните в службу поддержки и оставляете свой номер, чтобы они, когда будут доступны, перезвонили, вместо того, чтобы ждать их ответа.
Давайте посмотрим, как сделать то же самое, что и выше, используя Tornado:
Предпоследняя строка кода вызывает метод AsyncHTTPClient.fetch , который получает данные по URL-адресу неблокирующим способом. Этот метод выполняется и возвращается немедленно. Поскольку каждая следующая строка будет выполнена до того, как будет получен ответ по URL-адресу, невозможно получить объект, как результат выполнения метода. Решение этой проблемы заключается в том, что метод fetch вместо того, чтобы возвращать объект, вызывает функцию с результатом или обратный вызов. Обратный вызов в этом примере — handle_response .
В примере вы можете заметить, что первая строка функции handle_response проверяет наличие ошибки. Это необходимо, потому что невозможно обработать исключение. Если исключение было создано, то оно не будет отрабатываться в коде из-за цикла событий. Когда fetch выполняется, он запускает HTTP-запрос, а затем обрабатывает ответ в цикле событий. К тому моменту, когда возникнет ошибка, стек вызовов будет содержать только цикл событий и текущую функцию, при этом нигде в коде не сработает исключение. Таким образом, любые исключения, созданные в функции обратного вызова, прерывают цикл событий и останавливают выполнение программы. Поэтому все ошибки должны быть переданы как объекты, а не обработаны в виде исключений. Это означает, что если вы не проверили наличие ошибок, то они не будут обрабатываться.
Другая проблема с обратными вызовами заключается в том, что в асинхронном программировании единственный способ избегать блокировок — это обратный вызов. Это может привести к очень длинной цепочке: обратный вызов после обратного вызова после обратного вызова. Поскольку теряется доступ к стеку и переменным, вы в конечном итоге переносите большие объекты во все ваши обратные вызовы, но если вы используете сторонние API-интерфейсы, то не можете передать что-либо в обратный вызов, если он этого не может принять. Это также становится проблемой, потому что каждый обратный вызов действует как поток. Например, вы хотели бы вызвать три API-интерфейса и дождаться, пока все три вернут результат, чтобы его обобщить. В Gevent вы можете это сделать, но не с обратными вызовами. Вам придется немного поколдовать, сохраняя результат в глобальной переменной и проверяя в обратном вызове, является ли результат окончательным.
Сравнения
Если вы хотите предотвратить блокировку ввода-вывода, вы должны использовать либо потоки, либо асинхронность. В Python вы выбираете между зелеными потоками и асинхронным обратным вызовом. Вот некоторые из их особенностей:
Зеленые потоки
- потоки управляются на уровне приложений, а не аппаратно;
- включают в себя все проблемы потокового программирования.
Обратный вызов
- сопрограммы невидимы для программиста;
- обратные вызовы ограничивают использование исключений;
- обратные вызовы трудно отлаживаются.
Как решить эти проблемы?
Вплоть до Python 3.3 зеленые потоки и обратный вызов были оптимальными решениями. Чтобы превзойти эти решения, нужна поддержка на уровне языка. Python должен каким-то образом частично выполнить метод, прекратить выполнение, поддерживая при этом объекты стека и исключения. Если вы знакомы с концепциями Python, то понимаете, что я намекаю на генераторы. Генераторы позволяют функции возвращать список по одному элементу за раз, останавливая выполнение до того момента, когда следующий элемент будет запрошен. Проблема с генераторами заключается в том, что они полностью зависят от функции, вызывающей его. Другими словами, генератор не может вызвать генератор. По крайней мере так было до тех пор, пока в PEP 380 не добавили синтаксис yield from , который позволяет генератору получить результат другого генератора. Хоть асинхронность и не является главным назначением генераторов, они содержат весь функционал, чтобы быть достаточно полезными. Генераторы поддерживают стек и могут создавать исключения. Если бы вы написали цикл событий, в котором бы запускались генераторы, у вас получилась бы отличная асинхронная библиотека. Именно так и была создана библиотека Asyncio.
Все, что вам нужно сделать, это добавить декоратор @coroutine , а Asyncio добавит генератор в сопрограмму. Вот пример того, как обработать те же три URL-адреса, что и раньше:
Прим. перев. В примерах используется aiohttp версии 1.3.5. В последней версии библиотеки синтаксис другой.
Несколько особенностей, которые нужно отметить:
- ошибки корректно передаются в стек;
- можно вернуть объект, если необходимо;
- можно запустить все сопрограммы;
- нет обратных вызовов;
- строка 10 не выполнится до тех пор, пока строка 9 не будет полностью выполнена.
Единственная проблема заключается в том, что объект выглядит как генератор, и это может вызвать проблемы, если на самом деле это был генератор.
Async и Await
Библиотека Asyncio довольно мощная, поэтому Python решил сделать ее стандартной библиотекой. В синтаксис также добавили ключевое слово async . Ключевые слова предназначены для более четкого обозначения асинхронного кода. Поэтому теперь методы не путаются с генераторами. Ключевое слово async идет до def , чтобы показать, что метод является асинхронным. Ключевое слово await показывает, что вы ожидаете завершения сопрограммы. Вот тот же пример, но с ключевыми словами async / await:
Программа состоит из метода async . Во время выполнения он возвращает сопрограмму, которая затем находится в ожидании.
Заключение
В Python встроена отличная асинхронная библиотека. Давайте еще раз вспомним проблемы потоков и посмотрим, решены ли они теперь:
- процессорное переключение контекста: Asyncio является асинхронным и использует цикл событий. Он позволяет переключать контекст программно;
- состояние гонки: поскольку Asyncio запускает только одну сопрограмму и переключается только в точках, которые вы определяете, ваш код не подвержен проблеме гонки потоков;
- взаимная/активная блокировка: поскольку теперь нет гонки потоков, то не нужно беспокоиться о блокировках. Хотя взаимная блокировка все еще может возникнуть в ситуации, когда две сопрограммы вызывают друг друга, это настолько маловероятно, что вам придется постараться, чтобы такое случилось;
- исчерпание ресурсов: поскольку сопрограммы запускаются в одном потоке и не требуют дополнительной памяти, становится намного сложнее исчерпать ресурсы. Однако в Asyncio есть пул «исполнителей» (executors), который по сути является пулом потоков. Если запускать слишком много процессов в пуле исполнителей, вы все равно можете столкнуться с нехваткой ресурсов.
Несмотря на то, что Asyncio довольно хорош, у него есть и проблемы. Во-первых, Asyncio был добавлен в Python недавно. Есть некоторые недоработки, которые еще не исправлены. Во-вторых, когда вы используете асинхронность, это значит, что весь ваш код должен быть асинхронным. Это связано с тем, что выполнение асинхронных функций может занимать слишком много времени, тем самым блокируя цикл событий.
Существует несколько вариантов асинхронного программирования в Python. Вы можете использовать зеленые потоки, обратные вызовы или сопрограммы. Хотя вариантов много, лучший из них — Asyncio. Если используете Python 3.5, то вам лучше использовать эту библиотеку, так как она встроена в ядро python.
Асинхронная модель программирования
Большинство языков программирования по умолчанию поддерживают однопоточную синхронную модель программирования: методы выполняются последовательно в едином потоке выполнения, из которого они были вызваны. При использовании данной модели необходимо ждать, пока выполнится каждый метод, прежде чем будет запущен следующий. Если метод выполняется долгое время, например, если он загружает большие объемы данных или ждет ответа от сервера, поток выполнения блокируется, пока этот метод не закончит работу и не вернет управление. Для некоторых программ использование синхронной модели негативно влияет на производительность.
В другом распространенном подходе, асинхронной модели программирования, выполнение методов чередуется: можно перейти к другому методу до того, как текущий закончит работу. Все методы выполняются в едином потоке и явно передают ему управление: в любой момент времени можно быть уверенным, что выполняется ровно один метод. В сущности, асинхронное программирование — это программирование, где порядок исполнения неизвестен заранее.
В сравнении с синхронной моделью асинхронная работает лучше, когда:
— Есть большое количество методов, так что скорее всего постоянно существует хотя бы один метод, который ничего не ожидает и может продвигаться в выполнении.
— Методы выполняют много операций ввода/вывода, что заставило бы синхронную программу потратить много времени в заблокированном состоянии, ожидая данных, пока другие методы могли бы выполняться.
— Методы преимущественно независимы друг от друга, так что нет необходимости взаимодействия между ними (и, следовательно, нет необходимости одному методу ждать другой).
Модуль Asyncio в Python
Модуль asyncio был добавлен в основную библиотеку python в версии 3.4.
Asyncio использует однопоточный, однопроцессный подход, в котором части приложения взаимодействуют, чтобы явно переключать задачи в оптимальное время. Наиболее часто это переключение происходит, когда иначе программа бы была заблокирована, ожидая чтения или записи данных, syncio также включает поддержку планирования запуска кода в определенное время в будущем, для обеспечения ожидания одной сопрограммы (coroutine) завершения другой, для обработки системных сигналов и для распознавания событий, которые могут стать причиной смены исполняемых приложением задач.
Модуль предоставляет инфраструктуру для написания однопоточного конкурентного кода при помощи сопрограмм (corutines), мультиплексирования ввода/вывода данных через сокеты и другие ресурсы, запуска сетевых клиентов и серверов, и другие подобные примитивы.
Asyncio оперирует следующими терминами:
— event loop (цикл событий). Главная функция цикла событий — ожидание какого-либо события и определенная реакция на него. Доступно несколько реализаций цикла для эффективного использования преимуществ каждой операционной системы. Подходящяя реализация цикла по умолчанию выбирается автоматически, однако есть возможность явно выбрать нужную реализацию. Приложение взаимодействует с циклом событий, чтобы зарегистрировать код для выполнения, и дает циклу событий выполнять нужные вызовы кода приложения, когда ресурсы становятся доступны. Например, сетевой сервер открывает сокеты и регистрирует их, чтобы было обработано событие создания нового соединения или получения данных. Цикл событий называется циклом, потому что он постоянно собирает события и циклически проходит по ним, определяя, что делать с каждым событием: вызывает определенный код в ответ на события, которые ему известны.
— coroutine (сопрограмма) — специальная функция, которая возвращает управление объекту, вызвавшему её, сохраняя при этом своё состояние. При вызове функции-сопрограммы она не выполняется. Вместо этого она возвращает исполняемый объект, который передается циклу событий, и уже цикл событий ответственен за выполнение этого объекта немедленно или позже по расписанию. Сопрограммы похожи на функции-генераторы и могут быть реализованы как генераторы в версиях python ранее 3.5, в которых не было поддержки нативного синтаксиса сопрограмм.
— future — структура данных, представляющая собой результат работы, которая еще не была завершена. Цикл событий следит за future-объектами и ждет их завершения. Когда future завершает свою работу, он отмечается выполненым. Помимо этого asyncio поддерживает блокировки (locks) и семафоры (semaphores).
— task — представляет собой обертку сопрограммы и наследуется от Future. Задачу можно запланировать при помощи цикла событий, чтобы она выполнялась при наличии необходимых ресурсов, и производила результат, который мог быть использован другими корутинами.
— async и await — ключевые слова, появившиеся в python версии 3.5 для обозначения функций как сопрограмм для использования циклом событий.
Цикл событий занимается:
— Регистрацией, выполнением и отменой отложенных вызовов (таймаутов);
— Созданием клиентского и серверного “транспорта” для различных видов взаимодействий;
— Запуском подпроцессов и связанного “транспорта” для взаимодействия с внешними программами;
— Передачей дорогостоящих вызовов функций в пул потоков.
Пример простого приложения, использующего asyncio:
import asyncio
async def speak_async():
print(‘Inside coroutine’)loop = asyncio.get_event_loop()
loop.run_until_complete(speak_async())
loop.close()
Чтобы запустить выполнение сопрограмм, необходимо создать цикл событий и вызвать в нем сопрограммы. Вызов метода, определенного как сопрограмма, возвращает объект сопрограммы, который может использоваться циклом событий. Можно приостановить выполнение сопрограммы, используя ключевое слово await (или yield from в ранних версиях python). При выполнении следующего за ключевым словом выражения возможно переключение с текущей сопрограммы на другую или на основной поток выполнения. Выражение после ключевого слова await должно быть awaitable-объектом (другой сопрограммой или специальным объектом, у которого реализован метод __await__). При передаче управления в цикл событий, состояние процесса, приостановившего сопрограмму отслеживается. После его завершения цикл событий передает управление обратно в приостановленную сопрограмму, ожидающую результата, которая продолжает работу.
Python когда следует использовать async, а когда await?
А await ожидает завершения awaitable объекта и возвращает результат.
Их сложно перепутать: async — это часть утверждения (statement), await — унарный оператор, часть выражения (expression)
await , async for и async with должны использоваться только внутри асинхронной функции.
Попытаюсь дополнить ответ @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 ), друг без друга они не существуют.
Крайне внятной я нашел следующую информацию:
Не знаю, есть ли в интернете перевод этого добра, но если нужен, я мог бы подготовить.
Напоследок, интересное утверждение: «используйте асинхронность, когда можете, а потоки — когда должны».