Асинхронные возможности Python

Асинхронность:

Модель

<!> Предполагается, что весь предлагаемый код вы запускаете и смотрите на результат; без этого понять намного сложнее, if even possible ☺

Асинхронность как произвольное исполнение частей кода между yield-ами

Можно и дальше усложнять, но и так уже непросто!

Ещё модели

Ещё раз: асинхронность — это не параллелизм! Все фрагменты выполняются последовательно в один поток.

Синтаксис Async

<!> Если до этого момента не стало понятно:

Перепишем предыдущий пример на async

Примечание: @types.coroutine — низкоуровневая сопрограмма, которая может делать и return значение, и yield, то есть напрямую обращаться к образующему циклу. Встречается редко.

   1 from random import randint
   2 from string import ascii_uppercase
   3 from types import coroutine
   4 from collections import deque
   5 
   6 @coroutine
   7 def subr():
   8     return (yield int) * (yield str)
   9 
  10 async def task(num):
  11     res = ""
  12     for i in range(num):
  13         res += await subr()
  14     return res
  15 
  16 def loop(*tasks):
  17     queue, result = deque((task, None) for task in tasks), []
  18     print("Start:", *queue, sep="\n\t")
  19     idx = -1
  20     while queue:
  21         task, request = queue.popleft()
  22         if request is int:
  23             data = randint(1, 4)
  24         elif request is str:
  25             data = ascii_uppercase[idx := idx + 1]
  26         else:
  27             data = request
  28         try:
  29             request = task.send(data)
  30         except StopIteration as ret:
  31             result.append((task, ret.value))
  32             task.close()
  33         else:
  34             queue.append((task, request))
  35     return result
  36 
  37 print("Done:", *loop(task(10), task(3), task(5)), sep="\n\t")

<!> Формально говоря, awaitable object — это просто объект с методом .__await__(), возвращающим итератор. Однако логика работы этого метода диктуется управляющим циклом, это вам не .__call__ ☺. Вот пример реализации логики Future для управляющего цикла asyncio.

Asyncio

Немного истории:

Базовая документация

Основные понятия:

Asyncio — это:

Высокоуровневое API (введение)

Синхронизация

И толстый-толстый слой шоколада!

Д/З

  1. Попробовать прочитать всю документацию и прощёлкать всё, до чего дотянетесь.

  2. EJudge: SyncSeq 'Синхронизация последовательности'

    Напишите класс Seq(name), экземпляры которого обладают свойством синхронизации в порядке их создания. Единственный параметр — name — это уникальный идентификатор экземпляра. Класс должен предоставлять корутину .run(), которая в нужный момент выводит name на стандартный вывод; возвращает она тоже name.

    Input:

       1 import asyncio
       2 import random
       3 
       4 # …
       5 
       6 async def main(*names):
       7     random.seed(1337)
       8     random.shuffle(seq := [Seq(name) for name in names])
       9     shnames = [s.name for s in seq]
      10     print(*shnames)
      11     result = await asyncio.gather(*(s.run() for s in seq))
      12     print(*result)
      13 
      14 asyncio.run(main(1, 2, 3, 4, 5, 6, 7))  
    
    Output:

    1 6 4 2 3 7 5
    1
    2
    3
    4
    5
    6
    7
    1 6 4 2 3 7 5
  3. EJudge: FilterQueue 'Вас здесь не стояло!'

    Напишите класс FilterQueue со следующими свойствами:

    • Это потомок asyncio.Queue

    • В экземпляре класса атрибут очередь.window содержит первый элемент очереди, или None, если очередь пуста (просмотр очередь.window не влияет на состояние очереди)

    • С помощью операции фильтр in очередь можно определить, присутствуют ли в очереди такие элементы, что выражение фильтр(элемент) истинно

    • Метод .later() синхронно переставляет первый элемент очереди в её конец, или вызывает исключение asyncio.QueueEmpty, если очередь пуста

    • Метод .get() содержит необязательный параметр фильтр. Вызов очередь.get(фильтр) работает так:

      • Если в очереди нет элементов, на которых фильтр(элемент) истинно, работает как обычный .get().

      • Если в очереди есть элементы, на которых фильтр(элемент) истинно, переставляет первый элемент очереди в её конец до тех пор, пока фильтр(элемент) не истинно, а затем выполняет обычный .get().

    • Разрешается воспользоваться внутренним представлением Queue; код Queue можно посмотреть тут

    Input:

       1 async def putter(n, queue):
       2     for i in range(n):
       3         await queue.put(i)
       4 
       5 async def getter(n, queue, filter):
       6     for i in range(n):
       7         await asyncio.sleep(0.03)
       8         yield await queue.get(filter)
       9 
      10 async def main():
      11     queue = FilterQueue(10)
      12     asyncio.create_task(putter(20, queue))
      13     async for res in getter(20, queue, lambda n: n % 2):
      14         print(res)
      15 
      16 asyncio.run(main())
    
    Output:

    1
    3
    5
    7
    9
    11
    13
    15
    17
    4
    19
    12
    6
    16
    8
    14
    0
    10
    2
    18
  4. EJudge: OneDArcade 'Одноменрая аркада'

    Написать класс Monster(Имя, Позиция, Задержка, Сила), определяющий поведение монстра в одномерной аркаде.

    • Имя — строка, остальные параметры — натуральные числа.

    • Класс имеет также метод-корутину .run(начало_эпизода, конец_эпизода).

      • начало_эпизода и конец_эпизода — это asyncio барьеры, которые предполагается использовать в программе

      • При задержке == N корутина прибавляет 1 к Позиции монстра каждое N-е событие начало_эпизода, если этот монстр жив

    Написать также корутину game(монстры, начало_эпизода, конец_эпизода, эпоха), которая после каждого события конец_эпизода:

    • Просматривает массив монстров монстры

    • Находит в нём самую левую пару живых монстров, чья Позиция совпадает

      • Если таких больше двух, берутся первые два в порядке перечисления в массиве
    • Заставляет эту пару сражаться: если их Сила равна, умирают оба, если нет — сила выжившего уменьшается на значение силы умершего

    Работа game() заканчивается:

    • Если все монстры умерли, тогда она возвращает "All dead"

    • Если все монстры выжили после эпоха эпизодов, возвращает "All flee"

    • В противном случае возвращается список выживших монстров (через пробел с запятой)

    Корутина main() в тестах не меняется

    Input:

       1 async def main(*specs):
       2     monsters = [Monster(*spec) for spec in specs]
       3     animate, freeze = asyncio.Barrier(len(monsters) + 1), asyncio.Barrier(len(monsters) + 1)
       4     squad = [asyncio.create_task(m.run(animate, freeze)) for m in monsters]
       5     result = await game(monsters, animate, freeze, 10000)
       6     _ = [m.cancel() for m in squad]
       7     return result
       8 
       9 print(asyncio.run(main(("Kano", 1, 1, 20), ("Sonya", 2, 2, 15), ("Liu Kang", 4, 3, 10))))
      10 print(asyncio.run(main(("Sonya", 2, 2, 15), ("Johnny Cage", 1, 3, 1))))
      11 print(asyncio.run(main(("Kano", 1, 1, 20), ("Sonya", 2, 2, 15), ("Liu Kang", 4, 3, 10), ("Kabal", 5, 1, 5))))
      12 print(asyncio.run(main(("Milena", 1, 1, 15), ("Kitana", 2, 2, 15))))
    
    Output:

    Liu Kang
    All flee
    Liu Kang, Kabal
    All dead

LecturesCMC/PythonIntro2024/13_Async (последним исправлял пользователь FrBrGeorge 2024-12-14 21:28:36)