Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Yield from coroutine vs yield from task

Guido van Rossum, in his speech in 2014 on Tulip/Asyncio shows the slide:

Tasks vs coroutines

  • Compare:

    • res = yield from some_coroutine(...)
    • res = yield from Task(some_coroutine(...))
  • Task can make progress without waiting for it

    • As log as you wait for something else
      • i.e. yield from

And I'm completely missing the point.

From my point of view both constructs are identical:

In case of bare coroutine - It gets scheduled, so the task is created anyways, because scheduler operates with Tasks, then coroutine caller coroutine is suspended until callee is done and then becomes free to continue execution.

In case of Task - All the same - new task is schduled and caller coroutine waits for its completion.

What is the difference in the way that code executed in both cases and what impact it has that developer should consider in practice?

p.s.
Links to authoritative sources (GvR, PEPs, docs, core devs notes) will be very appreciated.

like image 933
Gill Bates Avatar asked Nov 22 '14 10:11

Gill Bates


People also ask

Is coroutine deprecated?

Deprecated since version 3.8: If any awaitable in aws is a coroutine, it is automatically scheduled as a Task. Passing coroutines objects to wait() directly is deprecated as it leads to confusing behavior.

Does await always yield?

If its target yield s, await "passes on" the suspension to its own caller. This allows to suspend an entire stack of coroutines that all await each other. If its target returns s, await catches the return value and provides it to its own coroutine.

How does yield from work in Python?

Yield is a keyword in Python that is used to return from a function without destroying the states of its local variable and when the function is called, the execution starts from the last yield statement. Any function that contains a yield keyword is termed a generator.

What Asyncio gather returns?

The asyncio. gather() returns the results of awaitables as a tuple with the same order as you pass the awaitables to the function.


2 Answers

For the calling side co-routine yield from coroutine() feels like a function call (i.e. it will again gain control when coroutine() finishes).

yield from Task(coroutine()) on the other hand feels more like creating a new thread. Task() returns almost instantly and very likely the caller gains control back before the coroutine() finishes.

The difference between f() and th = threading.Thread(target=f, args=()); th.start(); th.join() is obvious, right?

like image 84
Andrew Svetlov Avatar answered Sep 20 '22 03:09

Andrew Svetlov


The point of using asyncio.Task(coro()) is for cases where you don't want to explicitly wait for coro, but you want coro to be executed in the background while you wait for other tasks. That is what Guido's slide means by

[A] Task can make progress without waiting for it...as long as you wait for something else

Consider this example:

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()) 

Output:

dummy ran 

As you can see, test1 was never actually executed, because we didn't explicitly call yield from on it.

Now, if we use asyncio.async to wrap a Task instance around test1, the result is different:

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()) 

Output:

in test1 dummy ran 

So, there's really no practical reason for using yield from asyncio.async(coro()), since it's slower than yield from coro() without any benefit; it introduces the overhead of adding coro to the internal asyncio scheduler, but that's not needed, since using yield from guarantees that coro is going to execute, anyway. If you just want to call a coroutine and wait for it to finish, just yield from the coroutine directly.

Side note:

I'm using asyncio.async* instead of Task directly because the docs recommend it:

Don’t directly create Task instances: use the async() function or the BaseEventLoop.create_task() method.

* Note that as of Python 3.4.4, asyncio.async is deprecated in favor of asyncio.ensure_future.

like image 20
dano Avatar answered Sep 22 '22 03:09

dano