Hacker News new | ask | show | jobs
by enragedcacti 30 days ago
> Like, why can't my sync function await something asynchronous?

The answer, at least for Python, is that it is an intentional limitation because the alternatives introduce some quite bad trade-offs.

Option 1: your awaited promise goes into the main async event loop. This is bad because it means that your single-threaded sync function now needs to be thread-safe, and so does any sync code that calls your sync function despite it not even knowing that you're doing anything async. This is essentially unworkable without throwing away the option of writing non-thread-safe code.

Option 2: Your awaited promise goes into its own new event loop that only contains sibling and child promises. There's nothing technically stopping someone from doing this[1], but now you've lost a ton of the value of async because you will inevitably end up with a ton of siloed event loops that leave the process idle despite other async tasks existing that could run. Effective async code needs to share an event loop at as high of a level as possible, which means tainting as many methods with async as possible. At that point, you might as well enforce it at the language level and avoid the inevitable pain and fragmentation that comes from other devs across the ecosystem mixing sync and async code.

[1] https://pypi.org/project/nest-asyncio/

As explained by Guido: https://github.com/python/cpython/issues/66435#issuecomment-...

3 comments

I think the downsides of option 2 are overstated here. In lots of cases you don't care about the "value of async", you just want code that works well enough and option 2 does accomplish that in anything that is not perf critical.
I agree in isolation, and I have used nest-asyncio a couple of times where it really was a lot easier than the alternative, but from an ecosystem perspective I'm glad it isn't the default. Most of the time someone wants to do this it's a junior trying to work around a non-issue (e.g copy-pasting from a guide that includes asyncio.run()), and the trade-off is a massively increased surface for performance footguns throughout your code base and all the libraries you use. Linters could save you from the first case but it would be a lot more work to profile, track down, and fix spots in all your dependencies that cause your event loop to get fragmented.
Option 1 could be easily solved by having an atomic {} blocks that statically error if call any potentially async function in it. This is better as it document where an externally visible invariant is temporarily broken (i.e. reentrancy is required), instead of being implied by the code and potentially being broken a a future code change.

Implicit thread safety across async blocks of course break if you introduce actual shared multithreading in the language, while if you have atomic blocks at least you can build transactional memory on top.

It may depend on the runtime giving you a sync wait that doesn't deadlock the loop you came from. In JS you just can't. `dontawait` would need V8 to be a different VM.