Асинхронные возможности 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

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

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

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

Наглядно об асинхронности:

   1 import asyncio
   2 import time
   3 
   4 async def full_sync(n, t, s):
   5     for i in range(n):
   6         print(s, i)
   7         time.sleep(t)
   8 
   9 async def n_async(n, t, s):
  10     for i in range(n):
  11         print(s, i)
  12         await asyncio.sleep(0)
  13         time.sleep(t)
  14 
  15 async def full_async(n, t, s):
  16     for i in range(n):
  17         print(s, i)
  18         await asyncio.sleep(t)
  19 
  20 async def runner(coro, n, t):
  21     print(coro.__name__, time.ctime())
  22     async with asyncio.TaskGroup() as tg:
  23         for i in range(3):
  24             tg.create_task(coro(n, t, i))
  25     print(time.ctime())
  26 
  27 for coro in full_async, n_async, full_sync:
  28     asyncio.run(runner(coro, 2, 0.5))

Asyncio — это:

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

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

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

Python3.14+: Интроспекция запущенной asyncio-программы

Д/З

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

  2. EJudge: AsyncPoly 'Вычисление многочлена'

    Написать класс YesFuture и функцию parse_poly(многочлен, x) со следующими свойствами:

    • YesFuture — это класс, похожий на Future, только проще.

      • obj = YesFuture(значение=None) задаёт awaitable-объект obj, для которого await obj немедленно возвращает значение.

      • obj.set(новое_значение) подменяет значение

    • У parse_poly() два параметра.

      • Второй параметр — объект типа YesFuture.

      • Первый параметр — строка, в которой описан многочлен от x по стандартным правилам: знак * между необязательным коэффициентом и x не ставится, цифры степени — это unicode-цифры верхнего регистра (SUPERSCRIPT).

      • parse_poly() возвращает корутину, вычисляющую значение многочлена для текущего значения x.

    • При создании корутины вместо операций сложения, умножения и возведения в степень необходимо пользоваться только специальными корутинами Sum(a, b), Mul(a, b) и Pow(a, b), параметры которых — awaitable-объекты (другие корутины или YesFuture)

    Специальные корутины будут входить в каждый тест.

    Input:

       1 async def Sum(a, b):
       2     return await a + await b
       3 
       4 async def Mul(a, b):
       5     return await a * await b
       6 
       7 async def Pow(a, b):
       8     return await a ** await b
       9 
      10 async def Run(poly, *args):
      11     x = YesFuture()
      12     for arg in args:
      13         s = parse_poly(poly, x)
      14         x.set(arg)
      15         print(await s)
      16 
      17 asyncio.run(Run("3x⁵ + x² - 6x + 4", 4, 2))
    
    Output:

    3068
    92
  3. EJudge: GroupWork 'Бригада'

    Напишите класс Loop, являющийся параметрическим декоратором для корутин.

    • Задекорированная корутина вызывает исходную многократно; эти повторы назовём «шагами»
    • На каждом шаге возвращаемое значение исходной корутины опускается — до тех пор, пока оно не окажется равным None, после этого возвращается None.

    • Если задекорированных корутин несколько, они отрабатывают свои шаги строго по очереди: один шаг первой корутины, один — второй и т. д. до последней, затем второй шаг первой и т. д.
    • Как только одна из них возвращает None, все остальные вместо очередного шага тоже сразу возвращают None

    • Параметров у класса нет, он используется в качестве пространства имён.

    Гарантируется, что в образующий цикл добавлены все задекорированные корутины ровно по одному разу.

    Input:

       1 @Loop()
       2 async def getqA(queue):
       3     print("A Get", res := await queue.get())
       4     return res
       5 
       6 @Loop()
       7 async def getqB(queue):
       8     print("B Get", res := await queue.get())
       9     return res
      10 
      11 async def Run(*coros):
      12     queue = asyncio.Queue()
      13     for i in list(range(1, 6)) + [None]:
      14         await queue.put(i)
      15     async with asyncio.TaskGroup() as tg:
      16         for coro in coros:
      17             tg.create_task(coro(queue))
      18 
      19 asyncio.run(Run(getqA, getqB))
    
    Output:

    A Get 1
    B Get 2
    A Get 3
    B Get 4
    A Get 5
    B Get None
  4. EJudge: TwoWay 'Тамбур'

    Написать класс Portal, который работает так же, как Barrier, однако дополнительно имеет property .topic. Этот дескриптор по умолчанию равен None, однако вызов .wait(топик) с не-None параметром его меняет на топик. Главное свойство Portal состоит в том, что к моменту «прохождения портала» любым его клиентом значение topic должно быть равно заданному.

    • Предполагается, что из клиентов только один задаёт топик, остальные не меняют его (например, передают None)

    Input:

       1 import asyncio
       2 async def task(p, topic=None):
       3     await p.wait(topic)
       4     print(p.topic)
       5 
       6 async def runner(N):
       7     p = Portal(N)
       8     for j in range(3):
       9         async with asyncio.TaskGroup() as tg:
      10             for i in range(N):
      11                 tg.create_task(task(p, f"FLAG{j}" if i == N // 2 else None))
      12         await p.reset()
      13 
      14 asyncio.run(runner(5))
    
    Output:

    FLAG0
    FLAG0
    FLAG0
    FLAG0
    FLAG0
    FLAG1
    FLAG1
    FLAG1
    FLAG1
    FLAG1
    FLAG2
    FLAG2
    FLAG2
    FLAG2
    FLAG2

LecturesCMC/PythonIntro2025/13_Async (последним исправлял пользователь FrBrGeorge 2025-12-03 22:58:20)