3 ms·
I learned about the concept of async/await from JS and back then was really amazed by the elegance of it. By now, the downsides are well-known, but I think Pyt
by xg15 1y ago
I learned about the concept of async/await from JS and back then was really amazed by the elegance of it.
By now, the downsides are well-known, but I think Python's implementation did a few things that made it particularly unpleasant to use.
There is the usual "colored functions" problem. Python has that too, but on steroids: There are sync and async functions, but then some of the sync functions can only be called from an async function, because they expect an event loop to be present, while others must not be called from an async function because they block the thread or take a lot of CPU to run or just refuse to run if an event loop is detected. That makes at least four colors.
The API has the same complexity: In JS, there are 3 primitives that you interact with in code: Sync functions, async functions and promises. (Understanding the event loop is needed to reason about the program, but it's never visible in the code).
Whereas Python has: Generators, Coroutines, Awaitables, Futures, Tasks, Event Loops, AsyncIterators and probably a few more.
All that for not much benefit in everyday situations. One of the biggest advantages of async/await was "fearless concurrency": The guarantee that your variables can only change at well-defined await points, and can only change "atomically". However, python can't actually give the first guarantee, because threaded code may run in parallel to your async code. The second guarantee already comes for free in all Python code, thanks to the GIL - you don't need async for that.
- Retr0id 1y ago> some of the sync functions can only be called from an async function, because they expect an event loop to be present I recognise that this situation is possible, but I don't think I've ever seen it happen. Can you give an example?
- xg15 1y agoEverything that directly interacts with an event loop object and calls methods such as loop.call_soon() [1]. This is used by most of asyncio's synchronization primitives, e.g. async.Queue. A consequence is that you cannot use asyncio Queues to pass messages or work items between async functions and worker threads. (And of course you can't use regular blocking queues either, because they would block). The only solution is to build your own ad-hoc system using loop.call_soon_threadsafe() or use third-party libs like Janus[2]. [1] https://github.com/python/cpython/blob/e4e2390a64593b33d65567179265f9c2cd9acae1/Lib/asyncio/base_events.py#L817 https://github.com/python/cpython/blob/e4e2390a64593b33d6556... [2] https://github.com/aio-libs/janus https://github.com/aio-libs/janus
- gloomyday 1y agoI remember trying to use async in Python for the first time in 2017, and I actually found it easier to learn the basics of Go to create a coroutine, export it as a shared library, and create the bindings. I'm not exaggerating. If I remember correctly, the Python async API was still in experimental phase at that time.
- ratmeadow 1y agoI think Python async is pretty cool - much nicer than threading or multiprocessing - yet has a few annoying rough edges like you say. Some specific issues I run into every time: Function colours can get pretty verbose when you want to write functional wrappers. You can end up writing nearly the exact same code twice because one needs to be async to handle an async function argument, even if the real functionality of the wrapper isn't async. Coroutines vs futures vs tasks are odd. More than is pleasant, you have one but need the other for an API for no intuitive reason. Some waiting functions work on some types and not on others. But you can usually easily convert between them - so why make a distinction in the first place? I think if you create a task but don't await it (which is plausible in a server type scenario), it's not guaranteed to run because of garbage collection or something. That's weird. Such behaviour should be obviously defined in the API.
- xg15 1y agoI think the general idea of function colors has some merit - when done right, it's a crude way to communicate information about a function's expected runtime in a way that can be enforced by the environment: A sync function is expected to run short enough that it's not user-perceptible, whereas an async function can run for an arbitrary amount of time. In "exchange", you get tools to manage the async function while it runs. If a sync function runs too long (on the event loop) this can be detected and flagged as an error. Maybe a useful approach for a language would be to make "colors" a first-class part of the type system and support them in generics, etc. Or go a step further and add full-fledged time complexity tracking to the type system.
- munificent 1y ago> Maybe a useful approach for a language would be to make "colors" a first-class part of the type system and support them in generics, etc. Rust has been trying to do that with "keyword generics": https://blog.rust-lang.org/inside-rust/2023/02/23/keyword-generics-progress-report-feb-2023/ https://blog.rust-lang.org/inside-rust/2023/02/23/keyword-ge...
- lmm 1y ago> Maybe a useful approach for a language would be to make "colors" a first-class part of the type system and support them in generics, etc. This is what languages with higher-kinded types do and it's glorious. In Scala you write your code in terms of a generic monad and then you can reuse it for sync or async.
- int_19h 1y agoGenerators are orthogonal to all this. They are the equivalent of `function*` in JS. And yes, they are also coroutines, but experience has shown that keeping generators separate from generic async functions is more ergonomic (hence why C# and JS both do the same thing).
- xg15 1y agoTrue. I think the connection is more a historical one became the first async implementation was done using generators and lots of "yield from" statements AFAIK. But I think generators are still sometimes mentioned in tutorials for this reason.
- int_19h 1y agoImplementing what was essentially an equivalent of `await` on top of `yield` (before we got `yield from` even) was a favorite pastime at some point. I worked on a project that did exactly that for WinRT projection to Python. And before that there was Twisted. It's very tempting because it gets you like 90% there. But then eventually you want something like `async for` etc...
- nateglims 1y agoThe API complexity really threw me when I last tried async python. It's very different from other async systems and is incredibly different from gevent or twisted which were popular when I was last writing server python.
- codethief 1y ago> but then some of the sync functions can only be called from an async function, because they expect an event loop to be present I agree that that's annoying but tbh it sounds like any other piece of code to me that relies on global state. (Man, I can't wait for algebraic effects to become mainstream…)