The article’s understanding of Java’s Virtual Threads is incorrect. Virtual threads have a preemptive concurrency model, not a co-operative one, so suspension points are not only at calls to things like Future.get()
Sorry if I've got this wrong, but wouldn't the first example behave the same way in Javascript as well? The function parent() "awaits" the completion of child(), so it wouldn't be possible to interleave the print statements.
A few years ago I worked for this startup as a principal engineer. The engineering manager kept touting how he was from XYZ and that he ran Pythonista meetups and how vast his knowledge of Python was. We were building a security product and needed to scan hundreds of thousands of documents quickly so we built a fan out with coroutines. I came on board well into this effort to assist in adding another adapter to this other platform that worked similarly. After seeing all the coroutines, being pickled and stored in S3, so that nodes could “resume” if they crashed yet - not a single create_task was present. All of this awaiting, pickling, attempting to resume, check, stuff, report, pickle, happened synchronously.
When trying to point out the issue with the architecture and getting into a shouting match with Mr. Ego, I was let go.
Maybe I'm nitpicking a bit but the second concrete example shows different flow in the parent method compared to the first one. If the second example would be a modification of the first one like shown below the output would be the same in both cases.
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. Do people just somehow learn this just when they need it? Is anyone having fun here?
I may be misunderstanding it, but the examples shown don't seem to be illustrative? It seems reasonably obvious that the prints will happen in this order in any language, because in example 1 we are explicitly (a)waiting for the child to finish, and in example 2 both of the parent prints are above the await. So I don't feel like either of these makes the point the author is trying to get across?
I mean of course the post does have a very valid point but it almost repeats like a mantra if there are no
asyncio.create_task in your code it’s not concurrent.
I mean it should probably at least mention asyncio.gather which IMO would be also much better to explain the examples with
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?
Both examples do the same in C#, together with explanation that couroutine is executed synchronously until hitting a real pause point. Change `asyncio.create_task` to `Task.Run` and that's exactly the same behavior in the second example as well.
My New Year’s Resolution will be to give up complaining about this on hn, but for now:
I find ChatGPT’s style and tone condescending and bland to the point of obfuscating whatever was unique, thoughtful and insightful in the original prompt.
Trying to reverse-engineer the “Not this: That!” phrasing, artificial narrative drama & bizarre use of emphasis to recapture that insight and thought is not something I’m at all enthusiastic to do.
Perhaps a middle ground: HN could support a “prompt” link to the actual creative seed?
For anyone somewhat interested in task & scheduling I highly recommend phil-op tutorial on building a kernel - there is a late branch on the github that specifically focus on asynchronous task - I implemented multi-core this week and it high a profound enlightenment on what the f* was actually going on. This is something Claude / Gemini helped me achieve so I didn't require me to spend nights on kernel debugging methodology & documentation ; Best experience I could get on async
Ironically, by trying to explain awaitables in Python through comparison with other languages, the author shows how much he doesn’t understand the asynchronous models of other languages lol
Asynchronous and parallel programming are concepts I've never really learned, and admittedly scared to use, because I never really understand what the code is doing or even the right way of writing such code. Does anyone have any good resources that helped you learn this and have a good mental model of the concepts involved?
I'm pretty sure that this post is not right. JS behaves the same way Python does in this example. Not sure about C#, though I suspect it is no different.
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).
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&...
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.
32 comments
[ 0.16 ms ] story [ 46.7 ms ] threadThe example from this StackOverflow question might be a better demonstration: https://stackoverflow.com/q/63455683
Here’s a horror story for you.
A few years ago I worked for this startup as a principal engineer. The engineering manager kept touting how he was from XYZ and that he ran Pythonista meetups and how vast his knowledge of Python was. We were building a security product and needed to scan hundreds of thousands of documents quickly so we built a fan out with coroutines. I came on board well into this effort to assist in adding another adapter to this other platform that worked similarly. After seeing all the coroutines, being pickled and stored in S3, so that nodes could “resume” if they crashed yet - not a single create_task was present. All of this awaiting, pickling, attempting to resume, check, stuff, report, pickle, happened synchronously.
When trying to point out the issue with the architecture and getting into a shouting match with Mr. Ego, I was let go.
https://effection-www.deno.dev/
In Python there's asyncio vs threading, and I feel there's just too much to navigate to quickly get up and running. Do people just somehow learn this just when they need it? Is anyone having fun here?
https://www.reddit.com/r/rust/comments/8aaywk/async_await_in...
await asyncio.sleep(0)
doesn't yield control back to an event loop, then what the heck is it actually for?
I find ChatGPT’s style and tone condescending and bland to the point of obfuscating whatever was unique, thoughtful and insightful in the original prompt.
Trying to reverse-engineer the “Not this: That!” phrasing, artificial narrative drama & bizarre use of emphasis to recapture that insight and thought is not something I’m at all enthusiastic to do.
Perhaps a middle ground: HN could support a “prompt” link to the actual creative seed?
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:
It prints: So the author's point is that "other" can never appear in-between "parent before" and "child start".Edit: clarification
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:
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!):
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.