4 ms·
I 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
by ratmeadow 1y ago
I 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.
- tylerhou 1y ago> 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. Sorry for the possibly naive question. If I need to call a synchronous function from an async function, why can't I just call await on the async argument? def foo(bar: str, baz: int): # some synchronous work pass async def other(bar: Awaitable[str]): foo(await bar, 0)
- gcharbonnier 1y agoNothing and that’s the problem because even though you can do it, your event loop will block until foo has finished executing, meaning that in this thread no other coroutine will be executed in the meantime (an event loop runs in its own thread. Most of the time there is only the main thread thus a single event loop). This defeats the purpose of concurrent programming.
- everforward 1y ago> 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. I think that use case doesn't work well in async, because async effectively creates a tree of Promises that resolve in order. A task that doesn't get await-ed is effectively outside it's own tree of Promises because it may outlive the Promise it is a child of. I think the solution would be something like Linux's zombie process reaping, and I can see how the devs prefer just not running those tasks to dealing with that mess.
- xg15 1y agoNo, Python's system is more complex and unfortunately overloads "await" to do several things. If you just do async def myAsyncFunction(): ... await someOtherAsyncFunction() ... then the call to someOtherAsyncFunction will not spawn any kind of task or delegate to the event loop at all - it will just execute someOtherAsyncFunction() within the task and event loop iteration that myAsyncFunction() is already running in. This is a major difference from JS. If you just did someOtherAsyncFunction() without await, this would be a fire-and-forget call in JS, but in Python, it doesn't do anything. The statement creates a coroutine object for the someOtherAsyncFunction() call, but doesn't actually execute the call and instead just throws the object away again. I think this is what triggers the "coroutine is not awaited" warning: It's not complaining about fire-and-forget being bad style, it's warning that your code probably doesn't do what you think it does. The same pitfall is running things concurrently. In JS, you'd do: task1 = asyncFunc1(); task2 = asyncFunc2(); await task1; await task2; In Python, the functions will be run sequentially, in the await lines, not in the lines with the function calls. To actually run things in parallel, you have to to loop.create_task(asyncFunc()) or one of the related methods. The method will schedule a new task and return a future that you can await on, but don't have to. But that "await" would work completely differently from the previous awaits internally.
- everforward 1y agoI think this is semantically the same thing, though I'm sure your terminology is more correct (not an expert here). If you do `someOtherAsyncFunction()` without await and Python tried to execute similarly to a version with `await`, then the one without await would happen in the same task and event loop iteration but there's no guarantee that it's done by the time the outer function is. Thus the existing task/event loop iteration has to be kept alive or the non-await'ed task needs to be reaped to some other task/event loop iteration. > loop.create_task(asyncFunc()) This sort of intuitively makes sense to me because you're creating a new "context" of sorts directly within the event loop. It's similar-ish to creating daemons as children of PID 1 rather than children of more-ephemeral random PIDs.