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.
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 thecoroutine()
finishes.The difference between
f()
andth = threading.Thread(target=f, args=()); th.start(); th.join()
is obvious, right?As described in PEP 380, the accepted PEP document that introduced yield from, the expression
res = yield from f()
comes from the idea of the following loop:With this, things become very clear: if
f()
issome_coroutine()
, then the coroutine is executed. On the other hand, iff()
isTask(some_coroutine())
,Task.__init__
is executed instead.some_coroutine()
is not executed, only the newly created generator is passed as the first argument toTask.__init__
.Conclusion:
res = yield from some_coroutine()
=> coroutine continues execution and returns the next valueres = yield from Task(some_coroutine())
=> a new task is created, which stores a non-executedsome_coroutine()
generator object.The point of using
asyncio.Task(coro())
is for cases where you don't want to explicitly wait forcoro
, but you wantcoro
to be executed in the background while you wait for other tasks. That is what Guido's slide means byConsider this example:
Output:
As you can see,
test1
was never actually executed, because we didn't explicitly callyield from
on it.Now, if we use
asyncio.async
to wrap aTask
instance aroundtest1
, the result is different:Output:
So, there's really no practical reason for using
yield from asyncio.async(coro())
, since it's slower thanyield from coro()
without any benefit; it introduces the overhead of addingcoro
to the internalasyncio
scheduler, but that's not needed, since usingyield from
guarantees thatcoro
is going to execute, anyway. If you just want to call a coroutine and wait for it to finish, justyield from
the coroutine directly.Side note:
I'm using
asyncio.async
* instead ofTask
directly because the docs recommend it:* Note that as of Python 3.4.4,
asyncio.async
is deprecated in favor ofasyncio.ensure_future
.