Основы по созданию бота в ТГ на python
February 8, 2023

Урок по Asyncio

Словарик терминов

  • Asyncio - Python библиотека, которая отвечает за асинхронную работу.
  • Корутина (Coroutine) - Асинхронная функция-генератор. В отличие от обычных функций, корутины могут быть приостановлены и запущены заново.
  • async def - синтаксис при создании асинхронной функции.

Пример: async def my_async_func(): ...

  • await - синтаксис, который отвечает за передачу управления другой корутине.

Пример: await my_async_func()

В этом уроке мы поговорим про асинхронность в python. Важно понимать, что асинхронность не равно параллельность. Мы будем использовать библиотеку Aiogram, а для параллельности используется библиотека Multiprocessing.

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

Благодаря конкурентности можно поставить одновременно выполняться 2 задачи: большую и маленькую, и маленькая выполнится быстрее, независимо от того, в каком порядке они были вызваны.

Параллельность — выполнение 2 задач одновременно. Один работник не может выполнять параллельно 2 задачи, поэтому используем несколько (например, несколько ядер процессоров)

Асинхронность в py.

Стандартный пример выполнения функции:

Результат будет умножение переданных значений.

Генераторы же позволяют отдавать разные значения при "повторном" их вызове.

Подробнее ознакомиться с понятием генератор можете тут.

В примере выше происходит следующее:

  1. Обозначается функция, которая принимает аргумент a
  2. Начинается вечный цикл
  3. Функция превращается в генератор благодаря наличию yield
  4. Если выполнить функцию next и передать в нее этот генератор, то вернется значение a
  5. Если вызвать функцию next повторно на объект генератора, то сначала переменная a перезапишется и умножится на 5, а потом перейдет на следующую итерацию цикла
  6. Если вызвать функцию next (снова), то вернется новое значение a
  7. И так далее

Получается, несмотря на страшный цикл 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, в питоне на английском языке.
Также более подробно можно посмотреть Олега Молчанова на Ютубе