Подтвердить что ты не робот

Разница между сопрограммой и будущей/задачей в Python 3.5?

Скажем, у нас есть фиктивная функция:

async def foo(arg):
    result = await some_remote_call(arg)
    return result.upper()

Какая разница между:

coros = []
for i in range(5):
    coros.append(foo(i))

loop = get_event_loop()
loop.run_until_complete(wait(coros))

и

from asyncio import ensure_future

futures = []
for i in range(5):
    futures.append(ensure_future(foo(i)))

loop = get_event_loop()
loop.run_until_complete(wait(futures))

Примечание. В примере возвращается результат, но это не является предметом обсуждения. Когда значение возвращает значение, используйте gather() вместо wait().

Независимо от возвращаемого значения, я ищу ясность в ensure_future(). wait(coros) и wait(futures) оба запускают сопрограммы, поэтому, когда и почему сопрограмма должна быть завернута в ensure_future?

В принципе, какой правильный путь (tm) запускает кучу неблокирующих операций с использованием Python 3.5 async?

Для дополнительного кредита, что, если я хочу, чтобы пакетные звонки? Например, мне нужно вызвать some_remote_call(...) 1000 раз, но я не хочу раздавить веб-сервер/базу данных /etc с 1000 одновременными подключениями. Это можно выполнить с помощью потока или пула процессов, но есть ли способ сделать это с помощью asyncio?

4b9b3361

Ответ 1

Комментарий от Vincent связан с https://github.com/python/asyncio/blob/master/asyncio/tasks.py#L346, который показывает, что wait() обертывает сопрограммы в ensure_future() для вас!

Другими словами, нам нужно будущее, и сопрограммы будут безмолвно трансформироваться в них.

Я обновлю этот ответ, когда найду окончательное объяснение того, как выполнять сопрограммы/фьючерсы.

Ответ 2

Сопрограмма - это генераторная функция, которая может давать значения и принимать значения извне. Преимущество использования сопрограммы заключается в том, что мы можем приостановить выполнение функции и возобновить ее позже. В случае сетевой операции имеет смысл приостановить выполнение функции, пока мы ожидаем ответа. Мы можем использовать время для запуска некоторых других функций.

Будущее похоже на объекты Promise из Javascript. Это как заполнитель для значения, которое будет реализовано в будущем. В вышеупомянутом случае, ожидая сетевого ввода-вывода, функция может дать нам контейнер, обещая, что он заполнит контейнер значением, когда операция завершится. Мы держимся за будущий объект и, когда он выполняется, мы можем вызвать для него метод для получения фактического результата.

Прямой ответ: Вам не нужен ensure_future, если вам не нужны результаты. Они хороши, если вам нужны результаты или получены исключения.

Дополнительные кредиты: Я бы выбрал run_in_executor и пропустил экземпляр Executor, чтобы контролировать количество максимальных работников.

Пояснения и примеры кодов

В первом примере вы используете сопрограммы. Функция wait берет кучу сопрограмм и объединяет их вместе. Таким образом, wait() завершается, когда все сопрограммы исчерпаны (завершено/завершено, возвращая все значения).

loop = get_event_loop() # 
loop.run_until_complete(wait(coros))

Метод run_until_complete должен убедиться, что цикл активен, пока не завершится выполнение. Обратите внимание, что в этом случае вы не получаете результаты асинхронного выполнения.

Во втором примере вы используете функцию ensure_future, чтобы обернуть сопрограмму и вернуть объект Task, который является своего рода Future. Сопрограмму планируется выполнить в главном цикле событий при вызове ensure_future. Возвращенный объект future/task еще не имеет значения, но со временем, когда сетевые операции завершатся, будущий объект будет содержать результат операции.

from asyncio import ensure_future

futures = []
for i in range(5):
    futures.append(ensure_future(foo(i)))

loop = get_event_loop()
loop.run_until_complete(wait(futures))

Итак, в этом примере мы делаем то же самое, за исключением того, что мы используем фьючерсы, а не просто сопрограммы.

Давайте рассмотрим пример использования asyncio/coroutines/futures:

import asyncio


async def slow_operation():
    await asyncio.sleep(1)
    return 'Future is done!'


def got_result(future):
    print(future.result())

    # We have result, so let stop
    loop.stop()


loop = asyncio.get_event_loop()
task = loop.create_task(slow_operation())
task.add_done_callback(got_result)

# We run forever
loop.run_forever()

Здесь мы использовали метод create_task для объекта loop. ensure_future будет планировать задачу в главном цикле событий. Этот метод позволяет нам планировать сопрограмму в цикле, который мы выбираем.

Мы также видим концепцию добавления обратного вызова с использованием метода add_done_callback для объекта задачи.

Task - это done, когда сопрограмма возвращает значение, вызывает исключение или отменяется. Есть способы проверить эти инциденты.

Я написал несколько сообщений в блоге на эти темы, которые могут помочь:

Конечно, вы можете найти более подробную информацию в официальном руководстве: https://docs.python.org/3/library/asyncio.html

Ответ 3

Простой ответ

  • Вызов функции сопрограммы (async def) НЕ запускает ее. Он возвращает объекты сопрограммы, как функция генератора возвращает объекты генератора.
  • await извлекает значения из сопрограмм, то есть "вызывает" сопрограмму
  • eusure_future/create_task запланирует запуск сопрограммы в цикле событий на следующей итерации (хотя и не ожидает их завершения, как поток демона).

Некоторые примеры кода

Давайте сначала проясним некоторые термины:

  • функция сопрограммы, та, которую вы async def с;
  • объект сопрограммы, что вы получаете, когда вы "вызываете" функцию сопрограммы;
  • task, объект, обернутый вокруг объекта сопрограммы для запуска в цикле событий.

Случай 1, await на сопрограмме

Мы создаем две сопрограммы, await одна, и используем create_task для запуска другой.

import asyncio
import time

# coroutine function
async def p(word):
    print(f'{time.time()} - {word}')


async def main():
    loop = asyncio.get_event_loop()
    coro = p('await')  # coroutine
    task2 = loop.create_task(p('create_task'))  # <- runs in next iteration
    await coro  # <-- run directly
    await task2

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

вы получите результат:

1539486251.7055213 - await
1539486251.7055705 - create_task

Объясните:

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

Случай 2, передача управления в цикл событий

Если мы заменим основную функцию, мы увидим другой результат:

async def main():
    loop = asyncio.get_event_loop()
    coro = p('await')
    task2 = loop.create_task(p('create_task'))  # scheduled to next iteration
    await asyncio.sleep(1)  # loop got control, and runs task2
    await coro  # run coro
    await task2

вы получите результат:

-> % python coro.py
1539486378.5244057 - create_task
1539486379.5252144 - await  # note the delay

Объясните:

При вызове asyncio.sleep(1) элемент управления возвращается в цикл обработки событий, и цикл проверяет выполнение задач, а затем запускает задачу, созданную create_task.

Обратите внимание, что сначала мы вызываем функцию сопрограммы, но не await, поэтому мы просто создали одну сопрограмму, а не заставили ее работать. Затем мы снова вызываем функцию сопрограммы и заключаем ее в вызов create_task, creat_task фактически запланирует запуск сопрограммы на следующей итерации. Таким образом, в результате, create task выполняется до await.

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

Под капотом

loop.create_task на самом деле вызывает asyncio.tasks.Task(), который вызывает loop.call_soon. И loop.call_soon поместит задачу в loop._ready. Во время каждой итерации цикла он проверяет все обратные вызовы в loop._ready и запускает его.

asyncio.wait, asyncio.ensure_future и asyncio.gather фактически вызывают loop.create_task прямо или косвенно.

Также обратите внимание на документы:

Обратные вызовы вызываются в том порядке, в котором они зарегистрированы. Каждый обратный вызов будет вызван ровно один раз.

Ответ 4

Из BDFL [2013]

Задачи

  • Это сопрограмма, завернутая в Будущее
  • class Task является подклассом класса Future
  • Так что это работает с ожиданием !

  • Как он отличается от голого coroutine?
  • Он может добиться прогресса, не дожидаясь его
    • Пока вы ждете чего-то еще, т.е.
      • ждать [something_else]

Имея это в виду, ensure_future имеет смысл в качестве имени для создания задачи, так как в будущем результат будет вычислен ли не ждать вас его ( до тех пор, как вы ждете что - то). Это позволяет циклу событий завершить вашу задачу, пока вы ждете от других вещей. Обратите внимание, что в Python 3.7 create_task является предпочтительным способом обеспечения будущего.

Примечание. Я изменил "выход из" в слайдах Guido, чтобы "ждать" здесь для современности.