Выход из сопрограммы против выхода из задачи
Гвидо ван Россум в своей речи в 2014 году на Tulip/Asyncio показывает слайд:
Задачи против сопрограммы
И я совершенно не понимаю смысла.
С моей точки зрения обе конструкции идентичны:
В случае bare coroutine - он запускается по расписанию, поэтому задача создается так или иначе, потому что планировщик работает с задачами, тогда сопроцессор coroutine coroutine приостанавливается до тех пор, пока не будет выполнен запрос, и затем освободится для продолжения выполнения.
В случае Task
- все-таки - новая задача выбрана, а callout coroutine ждет ее завершения.
В чем разница в том, как код выполнялся в обоих случаях и какое влияние на него должен иметь разработчик на практике?
p.s.
Ссылки на авторитетные источники (GvR, PEP, docs, заметки основных разработчиков) будут очень оценены.
Ответы
Ответ 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()
.