1 of 36

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

asyncio, нативные корутины

2 of 36

Import antigravity

Asyncio, the concurrent Python programmer’s dream, write borderline synchronous code and let Python work out the rest, it’s import antigravity all over again…

2

3 of 36

Начнем с генераторов

Генератор - это функция, которая выдает последовательность результатов вместо одного значения

def countdown(n):

while n > 0:

yield n

n -= 1

>>> for i in countdown(5):

... print(i, end=” ”)

...

5 4 3 2 1

>>>

Вместо возврата значения вы генерируете ряд значений (используя оператор yield).

Как правило, вы подключаете его к циклу for

3

4 of 36

Генератор

Поведение сильно отличается от обычной функции

Вызов функции генератора создает объект генератора. Однако он не запускает функцию.

def countdown(n):

print("Counting down from", n)

while n > 0:

yield n

n -= 1

>>> x = countdown(10)

>>> x

<generator object at 0x58490>

>>> x.__next__()

Counting down from 10

10

4

5 of 36

Генераторы в качестве пайплайна

Идея: Вы можете сложить ряд функций генератора вместе в канал и перемещать элементы через него с помощью цикла for

5

6 of 36

Генераторы в качестве пайплайна

>>> def sub_gen():

... yield 1.1

... yield 1.2

...

>>> def gen():

... yield 1

... for i in sub_gen():

... yield i

... yield 2

...

6

>>> for x in gen():

... print(x)

...

1

1.1

1.2

2

7 of 36

yield from

>>> def sub_gen():

... yield 1.1

... yield 1.2

...

>>> def gen():

... yield 1

... yield from sub_gen()

... yield 2

...

7

>>> for x in gen():

... print(x)

...

1

1.1

1.2

2

8 of 36

Yield как выражение

>>> def sub_gen():

... yield 1.1

... yield 1.2

... return 'Done!'

...

>>> def gen():

... yield 1

... result = yield from sub_gen()

... print('<--', result)

... yield 2

...

8

>>> for x in gen():

... print(x)

...

1

1.1

1.2

<-- Done!

2

9 of 36

Напишем chain

>>> def chain(*iterables):

... for it in iterables:

... for i in it:

... yield i

...

>>> s = 'ABC'

>>> r = range(3)

>>> list(chain(s, r))

['A', 'B', 'C', 0, 1, 2]

9

10 of 36

Обход дерева

def tree(cls):

yield cls.__name__, 0

for sub_cls in cls.__subclasses__():

yield sub_cls.__name__, 1

def display(cls):

for cls_name, level in tree(cls):

indent = ' ' * 4 * level

print(f'{indent}{cls_name}')

if __name__ == '__main__':

display(BaseException)

10

$ python3 tree.py

BaseException

Exception

GeneratorExit

SystemExit

KeyboardInterrupt

11 of 36

Расширяя можно добиться такого

def sub_tree(cls):

for sub_cls in cls.__subclasses__():

yield sub_cls.__name__, 1

for sub_sub_cls in sub_cls.__subclasses__():

yield sub_sub_cls.__name__, 2

for sub_sub_sub_cls in sub_sub_cls.__subclasses__():

yield sub_sub_sub_cls.__name__, 3

11

12 of 36

Классическая корутина

Генераторы, способные потреблять и возвращать значения, являются сопрограммами (корутинами).

Понимание классических сопрограмм в Python сбивает с толку, потому что на самом деле они являются генераторами, используемыми по-другому.

“Сопрограмма” на самом деле является функцией генератора, созданной с помощью ключевого слова yield в ее теле. А “объект сопрограммы” физически является объектом генератора.

12

13 of 36

Вспомним пример

def make_averager():

total, counter = 0, 0

def averager(new_value):

nonlocal total, counter

total += new_value

counter += 1

return total / counter

return averager

13

g = make_averager()

print(g(10)) # 10.0

print(g(10)) # 10.0

print(g(40)) # 20.0

print(g(10)) # 17.5

14 of 36

Корутина

def averager():

total = 0.0

count = 0

average = 0.0

while True:

term = yield average

total += term

count += 1

average = total/count

14

>>> coro_avg = averager()

>>> next(coro_avg)

0.0

>>> coro_avg.send(10)

10.0

>>> coro_avg.send(30)

20.0

>>> coro_avg.send(5)

15.0

15 of 36

Разница сопрограмм и генераторов

Генераторы создают данные для итерации

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

Чтобы ваш мозг не взорвался, не смешивайте эти два понятия вместе

Сопрограммы не связаны с итерацией

15

16 of 36

Asyncio

Идея очень проста.

Есть цикл обработки событий. И у нас есть функции, которые выполняют асинхронные операции ввода-вывода.

Мы передаем свои функции циклу событий и просим его запустить их для нас. Цикл событий возвращает нам объект Future, словно обещание, что в будущем мы что-то получим.

Мы держимся за обещание, время от времени проверяем, имеет ли оно значение (нам очень не терпится), и, наконец, когда значение получено, мы используем его в некоторых других операциях

16

17 of 36

Нативные корутины

Функция сопрограммы, определенная с помощью async def.

Вы можете делегировать из собственной сопрограммы другой нативной сопрограмме, используя ключевое слово await, аналогично тому, как классические сопрограммы используют yield from.

Оператор async def всегда определяет нативную сопрограмму, даже если ключевое слово await не используется в ее теле.

Ключевое слово await нельзя использовать вне нативной сопрограммы.

17

18 of 36

Нативные корутины

Сердце asynciо — корутины (сoroutines)

Корутины — функции помеченные как async def; за кулисами построены на генераторах (generators)

Генераторы возвращают ленивые итераторы (объекты-генераторы)

Функции-корутины возвращают объекты-корутины

18

19 of 36

Event loop

Программа, использующая корутины, должна в main запустить event loop (цикл обработки событий)

Event loop запускается вызовом asyncio.run()

Event loop отвечает за запуск корутин, коллбэков, финализацию асинхронных генераторов и т.д.

Под разными ОС запускаются разные реализации еvent loop: ProactorEventLoop под Windows, SelectorEventLoop под Linux

19

20 of 36

Событийный цикл

  1. Инициализация событийного цикла
  2. Извлечение задачи из очереди
  3. Выполнение задачи или её приостановка до готовности
  4. Обработка завершенных операций
  5. Переход к следующей задаче или ожидание событий.
  6. Повтор.

20

21 of 36

Event loop API

asyncio.get_event_loop() # возвращает работающий event loop или создаёт его

asyncio.get_running_event_loop () # возвращает работающий event loop

loop.run_until_complete(fn()) # запускает еvent loop, работает до завершения fn

loop.close() # «закрывает» event loop

asyncio.run(fn()) # делает «всё» за нас

loop.create_task(fn()) # ставит в очередь корутин fn

loop.run_forever() # запускает event loop до тех пока не будет вызван stop()

loop.stop() # - явная остановка event loop

21

22 of 36

Асинхронная программа

22

23 of 36

RuntimeError: Event loop is closed на Windows

При использовании asyncio.run() на Windows вы столкнетесь с RuntimeError. Это связано с подкапотной реализацией ProactorEventLoop (который только на винде).

Есть несколько вариантов этого избежать:

  1. Использовать по-старому�loop = asyncio.get_event_loop()

loop.run_until_complete(main())

  • Использовать другой event loop:�asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())

asyncio.run(main())

23

24 of 36

Как запустить корутину: способы

asyncio.run(coro())

asyncio.create_task(coro())

await coro()

Есть и другие

24

25 of 36

Как запустить корутину 1

asyncio.run(coro())

Вызывается из обычной функции для управления объектом сопрограммы, который обычно является точкой входа для всего асинхронного кода в программе, как супервизор в этом примере. Этот вызов блокируется до тех пор, пока не вернется тело coro. Возвращаемое значение вызова run() - это то, что возвращает тело coro.

25

26 of 36

Как запустить корутину 2

asyncio.create_task(coro())

Вызывается из сопрограммы, чтобы запланировать выполнение другой сопрограммы в конечном итоге. Этот вызов не приостанавливает текущую сопрограмму. Он возвращает экземпляр задачи, объект, который обертывает объект сопрограммы и предоставляет методы для управления и запроса его состояния.

26

27 of 36

Как запустить корутину 3

await coro()

Вызывается из сопрограммы для передачи управления объекту сопрограммы, возвращаемому coro(). Это приостанавливает текущую сопрограмму до тех пор, пока не вернется тело coro. Значением выражения await является любое возвращаемое тело coro.

27

28 of 36

Блокирующие и неблокирующие операции

Проблема, которую пытается решить асинхронностью, — это блокировка ввода-вывода.

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

28

29 of 36

Выполнение ввода-вывода

Выполнение ввода-вывода обычно состоит из двух отдельных шагов:

1. Проверка устройства:

- Блокирующая: ожидание готовности устройства, или

- Не блокирующая: например, опрос периодически до готовности

2. Передача:

- Синхронная: выполнение операции (например, чтение или запись), инициированной программой, или

- Асинхронная: выполнение операции в ответ на событие из ядра (асинхронное выполнение, англ. asynchronous / управляемый событиями, англ. event driven)

29

30 of 36

Блокирующие операции?

На деле мы называем “неблокирующими” операциями те, которые мы можем раздробить на маленькие кусочки, из-за чего становится возможна многозадачность

30

31 of 36

Что выбрать

if io_bound:

if io_very_slow:

print("Use Asyncio")

else:

print("Use Threads")

else:

print("Multi Processing")

31

32 of 36

Немного о Task

Task наследует Future (обёртка)

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

loop.create_task() — создание таска из корутины

await asyncio.wait(iterable) - запускает корутины

results = await asyncio.gather(iterable)

for t in asyncio.as_completed((t1, t2)):

result = await t

32

33 of 36

Вспомним spinner

Необходимо в фоне посчитать запрос пользователя, но, чтобы пользователь знал, что приложение считает - в консоли необходимо крутить анимацию загрузки.

33

34 of 36

Упрощение работу с асинхронщиной

aiomisc - это библиотека с различными утилитами для asyncio

https://aiomisc.readthedocs.io/ru/latest/index.html

34

35 of 36

Полезные ссылки

35

36 of 36

Перейдем к примерам

36