Асинхронное программирование
asyncio, нативные корутины
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
Начнем с генераторов
Генератор - это функция, которая выдает последовательность результатов вместо одного значения
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
Генератор
Поведение сильно отличается от обычной функции
Вызов функции генератора создает объект генератора. Однако он не запускает функцию.
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
Генераторы в качестве пайплайна
Идея: Вы можете сложить ряд функций генератора вместе в канал и перемещать элементы через него с помощью цикла for
5
Генераторы в качестве пайплайна
>>> 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
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
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
Напишем 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
Обход дерева
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
Расширяя можно добиться такого
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
Классическая корутина
Генераторы, способные потреблять и возвращать значения, являются сопрограммами (корутинами).
Понимание классических сопрограмм в Python сбивает с толку, потому что на самом деле они являются генераторами, используемыми по-другому.
“Сопрограмма” на самом деле является функцией генератора, созданной с помощью ключевого слова yield в ее теле. А “объект сопрограммы” физически является объектом генератора.
12
Вспомним пример
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
Корутина
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
Asyncio
Идея очень проста.
Есть цикл обработки событий. И у нас есть функции, которые выполняют асинхронные операции ввода-вывода.
Мы передаем свои функции циклу событий и просим его запустить их для нас. Цикл событий возвращает нам объект Future, словно обещание, что в будущем мы что-то получим.
Мы держимся за обещание, время от времени проверяем, имеет ли оно значение (нам очень не терпится), и, наконец, когда значение получено, мы используем его в некоторых других операциях
16
Нативные корутины
Функция сопрограммы, определенная с помощью async def.
Вы можете делегировать из собственной сопрограммы другой нативной сопрограмме, используя ключевое слово await, аналогично тому, как классические сопрограммы используют yield from.
Оператор async def всегда определяет нативную сопрограмму, даже если ключевое слово await не используется в ее теле.
Ключевое слово await нельзя использовать вне нативной сопрограммы.
17
Нативные корутины
Сердце asynciо — корутины (сoroutines)
Корутины — функции помеченные как async def; за кулисами построены на генераторах (generators)
Генераторы возвращают ленивые итераторы (объекты-генераторы)
Функции-корутины возвращают объекты-корутины
18
Event loop
Программа, использующая корутины, должна в main запустить event loop (цикл обработки событий)
Event loop запускается вызовом asyncio.run()
Event loop отвечает за запуск корутин, коллбэков, финализацию асинхронных генераторов и т.д.
Под разными ОС запускаются разные реализации еvent loop: ProactorEventLoop под Windows, SelectorEventLoop под Linux
19
Событийный цикл
20
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
RuntimeError: Event loop is closed на Windows
При использовании asyncio.run() на Windows вы столкнетесь с RuntimeError. Это связано с подкапотной реализацией ProactorEventLoop (который только на винде).
Есть несколько вариантов этого избежать:
loop.run_until_complete(main())
asyncio.run(main())
23
Как запустить корутину: способы
asyncio.run(coro())
asyncio.create_task(coro())
await coro()
Есть и другие
24
Как запустить корутину 1
asyncio.run(coro())
Вызывается из обычной функции для управления объектом сопрограммы, который обычно является точкой входа для всего асинхронного кода в программе, как супервизор в этом примере. Этот вызов блокируется до тех пор, пока не вернется тело coro. Возвращаемое значение вызова run() - это то, что возвращает тело coro.
25
Как запустить корутину 2
asyncio.create_task(coro())
Вызывается из сопрограммы, чтобы запланировать выполнение другой сопрограммы в конечном итоге. Этот вызов не приостанавливает текущую сопрограмму. Он возвращает экземпляр задачи, объект, который обертывает объект сопрограммы и предоставляет методы для управления и запроса его состояния.
26
Как запустить корутину 3
await coro()
Вызывается из сопрограммы для передачи управления объекту сопрограммы, возвращаемому coro(). Это приостанавливает текущую сопрограмму до тех пор, пока не вернется тело coro. Значением выражения await является любое возвращаемое тело coro.
27
Блокирующие и неблокирующие операции
Проблема, которую пытается решить асинхронностью, — это блокировка ввода-вывода.
По умолчанию, когда ваша программа обращается к данным из источника ввода-вывода, она ожидает завершения этой операции, прежде чем продолжить выполнение программы.
28
Выполнение ввода-вывода
Выполнение ввода-вывода обычно состоит из двух отдельных шагов:
1. Проверка устройства:
- Блокирующая: ожидание готовности устройства, или
- Не блокирующая: например, опрос периодически до готовности
2. Передача:
- Синхронная: выполнение операции (например, чтение или запись), инициированной программой, или
- Асинхронная: выполнение операции в ответ на событие из ядра (асинхронное выполнение, англ. asynchronous / управляемый событиями, англ. event driven)
29
Блокирующие операции?
На деле мы называем “неблокирующими” операциями те, которые мы можем раздробить на маленькие кусочки, из-за чего становится возможна многозадачность
30
Что выбрать
if io_bound:
if io_very_slow:
print("Use Asyncio")
else:
print("Use Threads")
else:
print("Multi Processing")
31
Немного о 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
Вспомним spinner
Необходимо в фоне посчитать запрос пользователя, но, чтобы пользователь знал, что приложение считает - в консоли необходимо крутить анимацию загрузки.
33
Упрощение работу с асинхронщиной
aiomisc - это библиотека с различными утилитами для asyncio
34
Полезные ссылки
35
Перейдем к примерам
36