Live data from Hacker News

Await Is Not a Context Switch: Understanding Python's Coroutines vs. Tasks

mergify.com

41–50 of 79 posts

Re: Await Is Not a Context Switch: Understanding Python's Coroutines vs. Tasks

#41
post #26

> Awaiting a coroutine does not give control back to the event loop. I think this is a subtler point than one might think on first read, which is muddled due to the poorly chosen examples. Here's a better illustration: import asyncio async def child(): print("child start") await asyncio.sleep(0) print("child end") async def parent(): print("parent before") await child() # It prints: other parent before child start ot…

Doesn't this make await a no-op? In what way are async functions asynchronous if tasks do not run interleaved?

Re: Await Is Not a Context Switch: Understanding Python's Coroutines vs. Tasks

#44
post #26

> Awaiting a coroutine does not give control back to the event loop. I think this is a subtler point than one might think on first read, which is muddled due to the poorly chosen examples. Here's a better illustration: import asyncio async def child(): print("child start") await asyncio.sleep(0) print("child end") async def parent(): print("parent before") await child() # It prints: other parent before child start ot…

Doesn't this make await a no-op? In what way are async functions asynchronous if tasks do not run interleaved?

Tasks are async funcs that have been spawned with asyncio.create_task or similar, which then schedules its execution. A timer of zero doesn't spawn anything so the coroutine just executes in the same frame as the caller so yes it essentially a noop.

Re: Await Is Not a Context Switch: Understanding Python's Coroutines vs. Tasks

#45
post #12

Is this the same as the distinction between a model in which async functions "run synchronously to the first await" vs one in which they "always yield once before executing", as discussed here? https://www.reddit.com/r/rust/comments/8aaywk/async_await_in...

I think there are at least two degrees of freedom being discussed here, though I'm quite unsure, so someone please correct me if I'm wrong:

- What happens when MyCoroutine() is invoked (as an ordinary function invocation - no await etc.): does execution of the body start right then, or do you just get some sort of awaitable object that can be used to start it later?

- What happens when the result of said invocation is awaited (using your language's await operator plus any extra function calls needed to begin execution): is there a forced yield to the scheduler at any point here, or do you directly start executing the body?

The article seems to be treating the two as an indivisible pair, and thus discussing the behavior merely before and after the pair as a whole, but your link seems to be discussing the first?

Again, I'm quite unsure, so curious if anyone has thoughts.

Re: Await Is Not a Context Switch: Understanding Python's Coroutines vs. Tasks

#46
This article's incomplete/flawed, I'm afraid.

And like ... I take no pleasure in calling that out, because I have been exactly where the author is when they wrote it: dealing with reams of async code that doesn't actually make anything concurrent, droves of engineers convinced that "if my code says async/await then it's automagically performant a la Golang", and complex and buggy async control flows which all wrap synchronous, blocking operations in a threadpool at the bottom anyway.

But it's still wrong and incomplete in several ways.

First, it conflates task creation with deferred task start. Those two behaviors are unrelated. Calling "await asyncfunc()" spins the generator in asyncfunc(); calling "await create_task(asyncfunc())" does, too. Calling "create_task(asyncfunc())" without "await" enqueues asyncfunc() on the task list so that the event loop spins its generator next time control is returned to the loop.

Second, as other commenters have pointed out, it mischaracterizes competing concurrency systems (Loom/C#/JS).

Third, its catchphrase of "you must call create_task() to be concurrent" is incomplete--some very common parts of the stdlib call create_task() for you, e.g. asyncio.gather() and others. Search for "automatically scheduled as a Task" in https://docs.python.org/3/library/asyncio-task.html

Fourth--and this seems like a nitpicky edge case but I've seen a surprising amount of code that ends up depending on it without knowing that it is--"await on coroutine doesn't suspend to the event loop" is only usually true. There are a few special non-Task awaitables that do yield back to the loop (the equivalent of process.nextTick from JavaScript).

To illustrate this, consider the following code:

    async def sleep_loop():
        while True:
            await asyncio.sleep(1)
            print("Sleep loop")

    async def noop():
        return None

    async def main():
        asyncio.create_task(sleep_loop())
        while True:
            await noop()
As written, this supports the article's first section: the code will busy-wait forever in while-True-await-noop() and never print "Sleep loop".

Related to my first point above, if "await noop()" is replaced with "await create_task(noop())" the code will still busy loop, but will yield/nextTick-equivalent each iteration of the busy loop, so "Sleep loop" will be printed. Good so far.

But what if "await noop()" is replaced with "await asyncio.sleep(0)"? asyncio.sleep is special: it's a regular pure-python "async def", but it uses a pair of async intrinsic behaviors (a tasks.coroutine whose body is just "yield" for sleep-0, or a asyncio.Future for sleep-nonzero). Even if the busy-wait is awaiting sleep-0 and no futures/tasks are being touched, it still yields. This special behavior confuses several of the examples in the article's code, since "await returns-right-away" and "await asyncio.sleep(0)" are not behaviorally equivalent.

Similarly, if "await noop()" is replaced with "await asyncio.futures.Future()", the task runs. This hints at the real Python asyncio maxims (which, credit where it's due, the article gets pretty close to!):

    Async operations in Python can only interleave (and thus be concurrent) if a given coroutine's stack calls "await" on:
       1. A non-completed future.
       2. An internal intrinsic awaitable which yields to the loop.
       3. One of a few special Python function forms which are treated equivalently to the above.
     Tasks do two things:
       1. Schedule a coroutine to be "await"ed by the event loop itself when it is next yielded to.
       2. Provide a Future-based handle that can optionally be used to directly wait for that coroutine's completion when the loop runs it.
     (As underlined in the article) everything interesting with Python's async concurrency uses Tasks. 
     Wrapping Tasks are often automatically/implicitly created by the stdlib or other functions that run supplied coroutines.

Re: Await Is Not a Context Switch: Understanding Python's Coroutines vs. Tasks

#47
post #8

Personally, I've never been able to make async work properly with Python. In Node.js I can schedule enough S3 ListBucket network requests in parallel to use 100% of my CPU core, by just mapping an array of prefixes into an array of ListBucket Promises. I can then do a Promise.all() and let them happen. In Python there's asyncio vs threading, and I feel there's just too much to navigate to quickly get up and running.…

the trio library has an excellent tutorial that explains all of these concepts[0] even if you don't use trio and stick to the core python libs it's worth reading:

https://trio.readthedocs.io/en/stable/tutorial.html

Re: Await Is Not a Context Switch: Understanding Python's Coroutines vs. Tasks

#48
post #26

> Awaiting a coroutine does not give control back to the event loop. I think this is a subtler point than one might think on first read, which is muddled due to the poorly chosen examples. Here's a better illustration: import asyncio async def child(): print("child start") await asyncio.sleep(0) print("child end") async def parent(): print("parent before") await child() # It prints: other parent before child start ot…

> So the author's point is that "other" can never appear in-between "parent before" and "child start". But isn't it true for JavaScript too? So I don't really get the author's point... am I missing something or the author('s LLM?) forced a moot comparison to JavaScript? Edit: after reading the examples twice I am 99.9% sure it's slop and flagged it. Edit2: another article from the same author: https://mergify.com/blo…

> But isn't it true for JavaScript too?

You're right, the equivalent JS script produces the same sequence of outputs.

It turns out there is a way to emulate Python's asyncio.create_task().

Python:

  await asyncio.create_task(child())
JavaScript:

  const childTask = new Promise((resolve) => {
    setTimeout(() => child().then(resolve), 0)
  })
  await childTask

Re: Await Is Not a Context Switch: Understanding Python's Coroutines vs. Tasks

#49
post #28

Earlier quoted context omitted.

Classic. I had a similar run-in with the CTO of a company a decade or so ago who point blank refused to use version control for the source code. He insisted on having directories that got zipped and passed around from developer to developer and good luck figuring out how to integrate someone else's changes. I won that particular battle but it was uphill all the way, at least he had the grace to afterwards admit that…

That is a very different run in.

When you assume you know, you shut the door to the possible.

Ego is the worst thing an engineer can possess.

Re: Await Is Not a Context Switch: Understanding Python's Coroutines vs. Tasks

#50
The main takeaway for me is that in Python, this doesn't work:

    async def somethingLongRunning():
       ...

    x = somethingLongRunning()
    ... other work that will take a lot of time ...
    await x  # with the expectation that this will be instant if the other work was long enough
That's counterintuitive coming from other languages and seems to defeat one of the key benefits of async/await (easy writing of async operations)?

I've seen so many scripts where tasks that should be concurrent weren't simply because the author couldn't be arsed to deal with all the boilerplate needed for async. JavaScript-style async/await solves this.

Post reply on HN