How to Use the “async def” Expression in Python
You can define an asyncio coroutine using the async def expression.
In this tutorial, you will discover asyncio async def expressions in Python.
What is async def
The “async def” expression defines a coroutine.
Functions defined with async def syntax are always coroutine functions, even if they do not contain await or async keywords.
— Python Compound statements
A coroutine is a function or routine that can be suspended and resumed. As such it has more than one entry point.
Coroutines are a more generalized form of subroutines. Subroutines are entered at one point and exited at another point. Coroutines can be entered, exited, and resumed at many different points.
— Python Glossary
The async def defines a coroutine expression that returns a coroutine object.
A coroutine function can be used to create a coroutine that may be used as the entry point for an asyncio program by passing it to the asyncio.run() function.
A coroutine may also be scheduled and awaited by another coroutine or wrapped in a Task object and scheduled independently.
Now that we know what async def is, let’s look at how we might use it.
Run your loops using all CPUs, download my FREE book to learn how.
How to Use async def
We can use the async def to define a coroutine function.
Асинхронное программирование в Python: краткий обзор
Когда говорят о выполнении программ, то под «асинхронным выполнением» понимают такую ситуацию, когда программа не ждёт завершения некоего процесса, а продолжает работу независимо от него. В качестве примера асинхронного программирования можно привести утилиту, которая, работая асинхронно, делает записи в лог-файл. Хотя такая утилита и может дать сбой (например, из-за нехватки свободного места на диске), в большинстве случаев она будет работать правильно и ей можно будет пользоваться в различных программах. Они смогут её вызывать, передавая ей данные для записи, а после этого смогут продолжать заниматься своими делами.
Применение асинхронных механизмов при написании некоей программы означает, что эта программа будет выполняться быстрее, чем без использования подобных механизмов. При этом то, что планируется запускать асинхронно, вроде утилиты для логирования, должно быть написано с учётом возникновения нештатных ситуаций. Например, утилита для логирования, если место на диске закончилось, может просто прекратить логирование, а не «обваливать» ошибкой основную программу.
Выполнение асинхронного кода обычно подразумевает работу такого кода в отдельном потоке. Это — если речь идёт о системе с одноядерным процессором. В системах с многоядерными процессорами подобный код вполне может выполняться процессом, пользующимся отдельным ядром. Одноядерный процессор в некий момент времени может считывать и выполнять лишь одну инструкцию. Это напоминает чтение книг. Нельзя читать две книги одновременно.
Если вы читаете книгу, а кто-то даёт вам ещё одну книгу, вы можете взять эту вторую книгу и приступить к её чтению. Но первую придётся отложить. По такому же принципу устроено и многопоточное выполнение кода. А если бы несколько ваших копий читало бы сразу несколько книг, то это было бы похоже на то, как работают многопроцессорные системы.
Если на одноядерном процессоре очень быстро переключаться между задачами, требующими разной вычислительной мощности (например — между некими вычислениями и чтением данных с диска), тогда может возникнуть такое ощущение, что единственное процессорное ядро одновременно делает несколько дел. Или, скажем, подобное происходит в том случае, если попытаться открыть в браузере сразу несколько сайтов. Если для загрузки каждой из страниц браузер использует отдельный поток — тогда всё будет сделано гораздо быстрее, чем если бы эти страницы загружались бы по одной. Загрузка страницы — не такая уж и сложная задача, она не использует ресурсы системы по максимуму, в результате одновременный запуск нескольких таких задач оказывается весьма эффективным ходом.
Асинхронное программирование в Python
Изначально в Python для решения задач асинхронного программирования использовались корутины, основанные на генераторах. Потом, в Python 3.4, появился модуль asyncio (иногда его название записывают как async IO ), в котором реализованы механизмы асинхронного программирования. В Python 3.5 появилась конструкция async/await.
Для того чтобы заниматься асинхронной разработкой на Python, нужно разобраться с парой понятий. Это — корутины (coroutine) и задачи (task).
Корутины
Обычно корутина — это асинхронная (async) функция. Корутина может быть и объектом, возвращённым из корутины-функции.
Если при объявлении функции указано то, что она является асинхронной, то вызывать её можно с использованием ключевого слова await :
Такая конструкция означает, что программа будет выполняться до тех пор, пока не встретит await-выражение, после чего вызовет функцию и приостановит своё выполнение до тех пор, пока работа вызванной функции не завершится. После этого возможность запуститься появится и у других корутин.
Приостановка выполнения программы означает, что управление возвращается циклу событий. Когда используют модуль asyncio , цикл событий выполняет все асинхронные задачи, производит операции ввода-вывода и выполняет подпроцессы. В большинстве случаев для запуска корутин используются задачи.
Задачи
Задачи позволяют запускать корутины в цикле событий. Это упрощает управление выполнением нескольких корутин. Вот пример, в котором используются корутины и задачи. Обратите внимание на то, что сущности, объявленные с помощью конструкции async def — это корутины. Этот пример взят из официальной документации Python.
import asyncio import time async def say_after(delay, what): await asyncio.sleep(delay) print(what) async def main(): task1 = asyncio.create_task( say_after(1, 'hello')) task2 = asyncio.create_task( say_after(2, 'world')) print(f"started at ") # Ждём завершения обеих задач (это должно занять # около 2 секунд.) await task1 await task2 print(f"finished at ") asyncio.run(main())
Функция say_after() имеет префикс async , в результате перед нами — корутина. Если немного отвлечься от этого примера, то можно сказать, что данную функцию можно вызвать так:
await say_after(1, 'hello') await say_after(2, 'world')
При таком подходе, однако, корутины вызываются последовательно и на их выполнение уходит около 3 секунд. В нашем же примере осуществляется их конкурентный запуск. Для каждой из них используется задача. В результате время выполнения всей программы составляет около 2 секунд. Обратите внимание на то, что для работы подобной программы недостаточно просто объявить функцию main() с ключевым словом async . В подобных ситуациях нужно пользоваться модулем asyncio .
Если запустить код примера, то на экран будет выведен текст, подобный следующему:
started at 20:19:39 hello world finished at 20:19:41
Обратите внимание на то, что отметки времени в первой и последней строках отличаются на 2 секунды. Если же запустить этот пример с последовательным вызовом корутин, то разница между отметками времени составит уже 3 секунды.
Пример
В этом примере производится нахождение количества операций, необходимых на вычисление суммы десяти элементов последовательности чисел. Вычисления производятся, начиная с конца последовательности. Рекурсивная функция начинает работу, получая число 10, потом вызывает сама себя с числами 9 и 8, складывая то, что будет возвращено. Подобное продолжается до завершения вычислений. В результате оказывается, например, что сумма последовательности чисел от 1 до 10 составляет 55. При этом наша функция весьма неэффективна, здесь используется конструкция time.sleep(0.1) .
import time def fib(n): global count count=count+1 time.sleep(0.1) if n > 1: return fib(n-1) + fib(n-2) return n start=time.time() global count count = 0 result = fib(10) print(result,count) print(time.time()-start)
Что произойдёт, если переписать этот код с использованием асинхронных механизмов и применить здесь конструкцию asyncio.gather , которая отвечает за выполнение двух задач и ожидает момента их завершения?
import asyncio,time async def fib(n): global count count=count+1 time.sleep(0.1) event_loop = asyncio.get_event_loop() if n > 1: task1 = asyncio.create_task(fib(n-1)) task2 = asyncio.create_task(fib(n-2)) await asyncio.gather(task1,task2) return task1.result()+task2.result() return n
На самом деле, этот пример работает даже немного медленнее предыдущего, так как всё выполняется в одном потоке, а вызовы create_task , gather и прочие подобные создают дополнительную нагрузку на систему. Однако цель этого примера в том, чтобы продемонстрировать возможности по конкурентному запуску нескольких задач и по ожиданию их выполнения.
Итоги
Существуют ситуации, в которых использование задач и корутин оказывается весьма полезным Например, если в программе присутствует смесь операций ввода-вывода и вычислений, или если в одной и той же программе выполняются разные вычисления, можно решать эти задачи, запуская код в конкурентном, а не в последовательном режиме. Это способствует сокращению времени, необходимого программе на выполнение определённых действий. Однако это не позволяет, например, выполнять вычисления одновременно. Для организации подобных вычислений применяется мультипроцессинг. Это — отдельная большая тема.
Уважаемые читатели! Как вы пишете асинхронный Python-код?