Всегда ли ожидание дает другим задачам возможность выполнить? - PullRequest
3 голосов
/ 31 января 2020

Я хотел бы знать, какие гарантии дает python, когда событие l oop переключит задачи.

Насколько я понимаю, async / await значительно отличаются от потоков в что событие l oop не переключает задачу на основе квантования времени, а это означает, что, если задача не выдаст (await), она будет выполняться бесконечно. На самом деле это может быть полезно, потому что управлять асинхронными критическими разделами легче, чем с многопоточностью.

Что мне менее понятно, так это примерно следующее:

async def caller():
    while True:
        await callee()


async def callee():
    pass

В этом примере caller многократно await. Так что технически это уступает. Но я не уверен, разрешит ли это выполнение других задач в событии l oop, потому что оно дает только callee, а это никогда не дает.

Это если я ожидал callee внутри «критической секции», хотя я знаю, что она не будет блокировать, рискую ли я что-то еще неожиданное?

Ответы [ 2 ]

6 голосов
/ 31 января 2020

Вы правы, чтобы быть осторожным. caller возвращает значение callee и событие l oop. Затем событие l oop решает, какую задачу продолжить. Другие задачи могут (надеюсь) быть зажаты между вызовами на callee. callee необходимо дождаться фактической блокировки Awaitable, такой как asyncio.Future или asyncio.sleep(), , а не сопрограммы, иначе элемент управления не будет возвращен событию l oop до caller возвращает.

Например, следующий код завершит sh задачу caller2 до того, как начнет работать над задачей caller1. Поскольку callee2 по сути является функцией syn c, не ожидающей блокирующих операций ввода-вывода, следовательно, точка приостановки не создается и caller2 возобновляется сразу после каждого вызова callee2.

import asyncio
import time

async def caller1():
    for i in range(5):
        await callee1()

async def callee1():
    await asyncio.sleep(1)
    print(f"called at {time.strftime('%X')}")

async def caller2():
    for i in range(5):
        await callee2()

async def callee2():
    time.sleep(1)
    print(f"sync called at {time.strftime('%X')}")

async def main():
    task1 = asyncio.create_task(caller1())
    task2 = asyncio.create_task(caller2())
    await task1
    await task2

asyncio.run(main())

Результат:

sync called at 19:23:39
sync called at 19:23:40
sync called at 19:23:41
sync called at 19:23:42
sync called at 19:23:43
called at 19:23:43
called at 19:23:44
called at 19:23:45
called at 19:23:46
called at 19:23:47

Но если callee2 ожидает , как показано ниже, переключение задач произойдет, даже если оно ожидает asyncio.sleep(0), и задачи будут выполняться одновременно.

async def callee2():
    await asyncio.sleep(1)
    print('sync called')

Результат:

called at 19:22:52
sync called at 19:22:52
called at 19:22:53
sync called at 19:22:53
called at 19:22:54
sync called at 19:22:54
called at 19:22:55
sync called at 19:22:55
called at 19:22:56
sync called at 19:22:56

Это поведение не обязательно интуитивно понятно, но имеет смысл, учитывая, что asyncio был сделан для одновременной обработки операций ввода-вывода и работы в сети, а не как обычно синхронные python коды.

Еще одна вещь, на которую следует обратить внимание: это все еще работает, если callee ожидает сопрограмму, которая, в свою очередь, ожидает asyncio.Future, asyncio.sleep() или другую сопрограмму, которая ожидает одну из этих вещей в цепочке , Управление потоком будет возвращено событию l oop, когда ожидается блокировка Awaitable. Таким образом, следующее также работает.

async def callee2():
    await inner_callee()
    print(f"sync called at {time.strftime('%X')}")

async def inner_callee():
    await asyncio.sleep(1)
2 голосов
/ 31 января 2020

TLDR: Нет. Только сопрограммы и соответствующие им ключевые слова (await, async with, async for) включить приостановка. То, произойдет ли приостановка, зависит от используемой структуры, если вообще.

Сторонние асинхронные c функции / итераторы / контекстные менеджеры могут выступать в качестве контрольных точек; если вы видите await <something> или одного из его друзей, то это может быть контрольно-пропускной пункт. Поэтому, чтобы быть в безопасности, вы должны подготовиться к планированию или отмене, происходящим там.

[ Документация Trio ]


Синтаксис await Python является синтактическим c сахаром вокруг двух основных механизмов: yield до временно приостанавливает со значением и return до навсегда завершает со значением. Это то же самое, что, скажем, функция сопрограммы генератора может использовать:

def gencoroutine():
    for i in range(5):
        yield i  # temporarily suspend
    return 5     # permanently exit

Примечательно, что return не не подразумевает приостановку. Возможно, что сопрограмма генератора вообще никогда не будет yield.

Ключевое слово await (и его родной элемент yield from) взаимодействует с обоими механизмами yield и return:

  • Если его цель yield s, await "передает" приостановку своему вызывающему абоненту. Это позволяет приостановить весь стек сопрограмм, чтобы все await друг друга.
  • Если его цель returns s, await перехватывает возвращаемое значение и предоставляет его собственному сопрограммная. Это позволяет возвращать значение непосредственно «вызывающему» без приостановки.

Это означает, что await не не гарантирует, что приостановка произошла. Задача await может вызвать приостановку.


Сама по себе async def сопрограмма может только return без приостановки и await до разрешить приостановку. Он не может приостановиться сам по себе (yield не приостанавливается на событие l oop).

async def unyielding():
    return 2  # or `pass`

Это означает, что await из только сопрограмм делает никогда приостановить . Только определенные c ожидающие могут приостановить.


Приостановка возможна только для ожидающих с пользовательским __await__ методом. Они могут yield непосредственно к событию l oop.

class YieldToLoop:
     def __await__(self):
         yield   # to event loop
         return  # to awaiter

Это означает, что await, прямо или косвенно, ожидаемого фреймворка, приостановит .

Точная семантика приостановки зависит от используемой платформы asyn c. Например, sleep(0) вызывает приостановку или нет, или какую сопрограмму вместо этого запустить, зависит от структуры. Это также распространяется на асинхронные c итераторы и контекстные менеджеры - например, многие асинхронные c контекстные менеджеры будут приостанавливать либо при входе или выходе, но не при обоих.

Trio

Если вы вызываете асинхронную функцию c, предоставляемую Trio (await <something in trio>), и она не вызывает исключение, то она всегда действует как контрольная точка. (Если оно вызывает исключение, оно может действовать как контрольная точка или нет.)

Asyncio

sleep() всегда приостанавливает текущую задачу, разрешая другие задачи бежать.

...