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

Выход из сопрограммы против выхода из задачи

Гвидо ван Россум в своей речи в 2014 году на Tulip/Asyncio показывает слайд:

Задачи против сопрограммы

  • Для сравнения:

    • res = выход из some_coroutine (...)
    • res = выход из задачи (some_coroutine (...))
  • Задача может добиться прогресса, не дожидаясь ее

    • Как журнал, как вы ждете чего-то еще
      • то есть. выход из

И я совершенно не понимаю смысла.

С моей точки зрения обе конструкции идентичны:

В случае bare coroutine - он запускается по расписанию, поэтому задача создается так или иначе, потому что планировщик работает с задачами, тогда сопроцессор coroutine coroutine приостанавливается до тех пор, пока не будет выполнен запрос, и затем освободится для продолжения выполнения.

В случае Task - все-таки - новая задача выбрана, а callout coroutine ждет ее завершения.

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

p.s.
Ссылки на авторитетные источники (GvR, PEP, docs, заметки основных разработчиков) будут очень оценены.

4b9b3361

Ответ 1

Для вызывающей стороны совместная подпрограмма yield from coroutine() воспринимается как вызов функции (т.е. она снова получит контроль при завершении coroutine()).

yield from Task(coroutine()), с другой стороны, больше похоже на создание нового потока. Task() возвращает почти мгновенно и очень вероятно, что вызывающий абонент получает контроль до завершения coroutine().

Разница между f() и th = threading.Thread(target=f, args=()); th.start(); th.join() очевидна, правильно?

Ответ 2

Точка использования asyncio.Task(coro()) предназначена для случаев, когда вы не хотите явно ждать coro, но вы хотите, чтобы coro выполнялся в фоновом режиме, пока вы ждете других задач. Это то, что слайд Guido означает

[A] Task может добиться прогресса, не дожидаясь его... , пока вы ждете для чего-то еще

Рассмотрим следующий пример:

import asyncio

@asyncio.coroutine
def test1():
    print("in test1")


@asyncio.coroutine
def dummy():
    yield from asyncio.sleep(1)
    print("dummy ran")


@asyncio.coroutine
def main():
    test1()
    yield from dummy()

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

Вывод:

dummy ran

Как вы можете видеть, test1 никогда не выполнялся, потому что мы явно не называли на него yield from.

Теперь, если мы используем asyncio.async для обтекания экземпляра Task вокруг test1, результат отличается:

import asyncio

@asyncio.coroutine
def test1():
    print("in test1")


@asyncio.coroutine
def dummy():
    yield from asyncio.sleep(1)
    print("dummy ran")


@asyncio.coroutine
def main():
    asyncio.async(test1())
    yield from dummy()

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

Вывод:

in test1
dummy ran

Таким образом, практической причины использования yield from asyncio.async(coro()) не существует, так как она медленнее, чем yield from coro() без какой-либо выгоды; он вводит накладные расходы при добавлении coro к внутреннему планировщику asyncio, но это не нужно, поскольку использование yield from гарантирует, что coro будет выполняться, в любом случае. Если вы просто хотите вызвать сопрограмму coroutine и дождаться ее завершения, просто yield from сопрограмму напрямую.

Боковое примечание:

Я использую asyncio.async * вместо Task непосредственно потому что документы рекомендуют его:

Не создавайте напрямую экземпляры Task: используйте функцию async() или метод BaseEventLoop.create_task().

* Обратите внимание, что с Python 3.4.4 asyncio.async устарел в пользу asyncio.ensure_future.

Ответ 3

Как описано в PEP 380, принятый документ PEP, в который введен результат, выражение res = yield from f() исходит из идеи следующего цикла:

for res in f():
    yield res

С этим все становится очень ясно: если f() есть some_coroutine(), то выполняется сопрограмма. С другой стороны, если f() - Task(some_coroutine()), вместо этого выполняется Task.__init__. some_coroutine() не выполняется, только первый созданный генератор передается как первый аргумент Task.__init__.

Вывод:

  • res = yield from some_coroutine() = > coroutine продолжает выполнение и возвращает следующее значение
  • res = yield from Task(some_coroutine()) = > создается новая задача, в которой хранится неиспользуемый объект-генератор some_coroutine().