В показанном коде нет причин использовать явное будущее, и 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