Урок по Asyncio
Словарик терминов
- Asyncio - Python библиотека, которая отвечает за асинхронную работу.
- Корутина (Coroutine) - Асинхронная функция-генератор. В отличие от обычных функций, корутины могут быть приостановлены и запущены заново.
- async def - синтаксис при создании асинхронной функции.
Пример: async def my_async_func(): ...
В этом уроке мы поговорим про асинхронность в python. Важно понимать, что асинхронность не равно параллельность. Мы будем использовать библиотеку Aiogram, а для параллельности используется библиотека Multiprocessing.
Конкурентность — разбиение задач на блоки и определение того, как будет осуществляться переключение между этими задачами. Другими словами, 2 большие задачи может выполнять один и тот же работник, переключаясь между ними.
Благодаря конкурентности можно поставить одновременно выполняться 2 задачи: большую и маленькую, и маленькая выполнится быстрее, независимо от того, в каком порядке они были вызваны.
Параллельность — выполнение 2 задач одновременно. Один работник не может выполнять параллельно 2 задачи, поэтому используем несколько (например, несколько ядер процессоров)
Асинхронность в py.
Стандартный пример выполнения функции:
Генераторы же позволяют отдавать разные значения при "повторном" их вызове.
Подробнее ознакомиться с понятием генератор можете тут.
В примере выше происходит следующее:
- Обозначается функция, которая принимает аргумент a
- Начинается вечный цикл
- Функция превращается в генератор благодаря наличию
yield - Если выполнить функцию
nextи передать в нее этот генератор, то вернется значениеa - Если вызвать функцию
nextповторно на объект генератора, то сначала переменнаяaперезапишется и умножится на 5, а потом перейдет на следующую итерацию цикла - Если вызвать функцию
next(снова), то вернется новое значениеa - И так далее
Получается, несмотря на страшный цикл while True:, этот цикл приостанавливался на строчке с yield. Программа возвращает контроль управления после этой команды. И когда вы в следующий раз запустите функцию next, функция generator продолжит выполнение с того места, где она закончила в прошлый раз.
Главное использовать тот же объект генератора g, так как именно в нем сохраняется предыдущее состояние, а не в самой функции. Нужен именно объект!
Поэтому, в этом примере переменная а сначала умножается, а потом вернется опять к началу цикла и опять приостановится на следующем yield.
Теперь к библиотеки Asyncio
Если обычные генераторы создаются благодаря наличию yield, то в Python 3.5 обновился синтаксис и появилось наличие слов async, await.
Теперь для создания нативной корутины для работы с асинхронностью, вы должны делать это так:
Если вы не прописали async def, то слово await в синхронном коде вызовет синтаксическую ошибку (SyntaxError).
А само слово await позволяет выполнить другую корутину и передать управление дальше. То есть, чтобы сделать ваш синхронный код асинхронным, вам недостаточно просто сделать приставку async перед функцией, а необходимо еще в каком-либо месте передать управление дальше.
Важно понимать один момент: если вы просто пытаетесь запустить корутины одну за одной, то они будут выполняться последовательно, а не конкурентно.
Они будут выполняться как в обычном синхронном блокирующем коде.
Контекстные переменные
Учитывая, что разные задачи выполняются конкурентно, но всё-таки отдельно, в Python есть такое понятие как "контекст".
Если вы в одной корутине вызываете другую, то они делят между собой один контекст, и в этот контекст можно размещать отдельные переменные, чтобы потом их доставать там, где вам это понадобится.
Контекстные переменные — это что-то вроде глобальных переменных, только доступных не во всем коде, а только в необходимом контексте.
Это бывает полезно, когда вы не хотите передавать переменную в другую функцию явно, например, если таких переменных у вас много и вы наверняка не знаете, где именно вам может понадобиться та или иная переменная.
Пример:
# Импортирую класс контекстных переменных
from contextvars import ContextVar
# Создаю объект счетчика с помощью контекстной переменной, можно обозначить значение по умолчанию = 0
MyCounter = ContextVar('counter', default=0)Теперь мы должны там, где нам нужно, задавать объект с помощью метода MyCounter.set() и получать с помощью метода MyCounter.get(). Методы сами по себе синхронные, поэтому await не нужен.
Сделаем функцию, которая будет увеличивать наш счетчик:
async def increase():
# Забираем из контекста объект счетчика
my_counter = MyCounter.get()
# Увеличиваем на 1
my_counter += 1
# Обязательно погружаем обратно в контекст
MyCounter.set(my_counter)Теперь сделаем функцию-цикл, где будет вызываться метод increase:
async def count():
while True:
# Увеличиваем значение счетчика
await increase()
# Забираем значение счетчика из контекста
my_counter = MyCounter.get()
print(f"Счетчик: {my_counter}")
await asyncio.sleep(1 / 10)
asyncio.run(count())Как мы видим, благодаря ContextVar-ам нам необязательно передавать переменную счетчика (my_counter) в функцию increase, чтобы с ней что-нибудь сделать.
В данном примере это не совсем оправдывает себя, но если вложенность у вас большая, то такой способ бывает полезен.
Пример с разными контекстами
Давайте примем в функцию count произвольную задержку и запустим 2 разных контекста с помощью функции asyncio.gather
async def count(delay):
while True:
await increase()
my_counter = MyCounter.get()
print(f"Счетчик с задержкой {delay:.2f} сек: {my_counter}")
await asyncio.sleep(delay)
async def main():
# Первый контекст с задержкой 0.33 сек, а второй - 0.2 сек
await asyncio.gather(
count(0.33),
count(0.2)
)
asyncio.run(main())Подведем итоги
1. Асинхронность в Python осуществляется с помощью общего потока, где задачи работают конкурентно в разных контекстах.
2. Конкурентность достигается управляемым переключением между разными задачами-контекстами
3. Асинхронность работает по принципу схожему с генераторами, которые работают в общем потоке
4. Для создания асинхронных функций используется кейворд async
5. Результатом вызова асинхронной функции является объект Coroutine.
6. Переключение между задачами происходит с помощью кейворда await.
7. Запустить задачи асинхронно (или конкурентно) можно с помощью asyncio.gather.
8. Так как весь написанный код работает последовательно в одном потоке, вызов блокирующих функций (вроде time.sleep) блокирует весь поток (event loop) до окончания работы такой функции.
Дополнительная литература
Всё про асинхронность от Лукаш Ланга, который проделал большую работу в Facebook и Instagram, в питоне на английском языке.
Также более подробно можно посмотреть Олега Молчанова на Ютубе