Каков вариант использования future.add_done_callback()?

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

Версия обратного вызова:

def bar(future):
    # do stuff using future.result()
    ...

async def foo(future):
    await asyncio.sleep(3)
    future.set_result(1)

loop = asyncio.get_event_loop()
future = loop.create_future()
future.add_done_callback(bar)
loop.run_until_complete(foo(future))

Альтернатива:

async def foo():
    await asyncio.sleep(3)
    bar(1)

loop = asyncio.get_event_loop()
loop.run_until_complete(foo())

Когда вторая версия будет недоступна/подходит?


person JSStuball    schedule 10.12.2018    source источник


Ответы (1)


В показанном коде нет причин использовать явное будущее, и add_done_callback вы всегда можете await. Более реалистичный вариант использования, если бы ситуация была обратной, если бы bar() породил foo() и нуждался в доступе к его результату:

def bar():
    fut = asyncio.create_task(foo())
    def when_finished(_fut):
        print("foo returned", fut.result())
    fut.add_done_callback(when_finished)

Если это напоминает вам об «аде обратных вызовов», вы на правильном пути — Future.add_done_callback является грубым эквивалентом оператора then в pre-async/await обещаниях JavaScript. (Детали различаются, потому что then() — это комбинатор, возвращающий другое обещание, но основная идея та же.)

Большая часть asyncio реализована в этом стиле с использованием неасинхронных функций, которые организуют асинхронное будущее. Этот базовый уровень транспортов и протоколов выглядит как модернизированная версия Twisted, с сопрограммами и потоками, реализованными как отдельный уровень поверх него, сахар более высокого уровня. Код приложения, написанный с использованием базового набора инструментов, выглядит вот так.

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

def bar():
    async def coro():
        ret = await foo()
        print("foo returned", ret)
    asyncio.create_task(coro())

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

Итак, какие же тогда являются варианты использования, когда нужно использовать Future API и add_done_callback? Я могу думать о нескольких:

  • Написание новых комбинаторов.
  • Соединение кода сопрограмм с кодом, написанным в более традиционном стиле обратного вызова, например this или это.
  • Написание кода на Python/C, где async def недоступен.

Чтобы проиллюстрировать первый пункт, подумайте, как бы вы реализовали такую ​​функцию, как asyncio.gather(). Он должен разрешать запуск переданных сопрограмм/фьючерсов и ждать, пока все не закончатся. Здесь add_done_callback — очень удобный инструмент, позволяющий функции запрашивать уведомления от всех фьючерсов, не ожидая их последовательно. В своей самой простой форме, которая игнорирует обработку исключений и различные функции, gather() может выглядеть так:

async def gather(*awaitables):
    loop = asyncio.get_event_loop()
    futs = list(map(asyncio.ensure_future, awaitables))
    remaining = len(futs)
    finished = loop.create_future()
    def fut_done(fut):
        nonlocal remaining
        remaining -= 1
        if not remaining:
            finished.set_result(None)  # wake up
    for fut in futs:
        fut.add_done_callback(fut_done)
    await finished
    # all awaitables done, we can return the results
    return tuple(f.result() for f in futs)

Даже если вы никогда не используете add_done_callback, это хороший инструмент для понимания и ознакомления с ним в той редкой ситуации, когда он вам действительно нужен.

person user4815162342    schedule 10.12.2018