4 ms·
> But when you're honest with yourself and interpret the word "async" as "expensive", or as "does network calls", it becomes clear that "async/await" makes impo
by klabb3 2y ago
> But when you're honest with yourself and interpret the word "async" as "expensive", or as "does network calls", it becomes clear that "async/await" makes important features of your function explicit, rather than implicit.
The main issue here is that whether or not a function is expensive is known upfront. In practice, this bites you when (a) you refactor (the impl changes), and (b) when you’re writing interfaces (the impl is unknown). This causes cumbersome cascading refactors and even complete loss of interop (in case where a 3p interface is sync and your impl is not).
In order to argue against that point, you have to argue that async is upfront-predictable and obviously part of the interface. And that it should be considered a breaking change - or semantically different if you will.
- shadowgovt 2y agoKey to note here is that asynchronicity impacts interface because it's an entirely different kind of function in a way that "regular" functions are not. Regular functions always return (or crash the program). Regular functions run to completion before returning control to the caller. These are two key properties async functions lack, so the caller has to care. It's analogous to the notion that checked exceptions are challenging because they also become part of the interface and so the caller is impacted by implementation details. (This does raise the question of whether there's a form of async hiding just out of sight of current language designs that's analogous to "unchecked exceptions": can one construct a model of function calling that makes one not have to care about async vs. sync functions at the cost of something else? I suspect, without exploring too deeply, that Orc (https://orc.csres.utexas.edu/ https://orc.csres.utexas.edu/) has gone that road).
- klabb3 2y ago> it's an entirely different kind of function Absolutely. Async is sugar for state machines. There is nothing wrong with that per se. > Regular functions run to completion before returning control to the caller. Yep. However it's important to note that it’s very possible to cancel regular functions IO from a language perspective, using a cooperative signal. (I always squirm a little every time someone says cancelation is easier in async - you shouldn’t imo cancel by dropping a state machine on the floor). The best example of this is context.Context in Golang. But even Tokio has CancellationToken. That said, the real benefit of the state machine is known size. This is the main (and I would say only) meaningful advantage compared to a regular stack. In Golang, you have dynamic per-goroutine (green thread) stack, which is wasting memory compared to rust async.
- enragedcacti 2y ago> can one construct a model of function calling that makes one not have to care about async vs. sync functions at the cost of something else? If I understand it correctly this is technologically possible in Python and there are two main tradeoffs depending on how the event loop is modified. given the following: x = 0 y = 0 async def set_x(): global x x = await request_x() # returns 5 async def set_y() global y y = await request_y() # returns 10 def business_logic(): if x == 0: set_y() # magic synchronous call to async func print(x + y) async def do_business(): business_logic() async def main(): await asyncio.gather( set_x(), do_business()) What is the value that's printed? If all coroutines share the same event loop then we don't know because set_y() could yield execution and give it to set_x(). The author of business_logic() may not even know they are writing async code because it just works, and the author of main() assumes that do_business() is concurrency safe because it's async. The alternative is nested event loops, so everything in business_logic() gets its own event loop that must fully complete before anything else in the parent event loop can run. This solves the concurrency bug but we've introduced blocking into code that was obviously intended to be async. This approach works (see nest-asyncio package) but the core devs have opted against it because they feel it makes it too easy to break the value of async on accident or out of laziness. IMO something that could work is a `sync def` declaration where the function is written in async style but gets its own event loop and can be called synchronously. It solves the tainting issue while still being explicit, easy to convert to full async, and easy to lint for in projects where it would be a net negative.
- dragonwriter 2y ago> Regular functions always return (or crash the program). No, you can't say that; claiming they do is equivalent to claiming the halting problem is trivially solvable since any program it applies to will definitively halt. Trivial counterexample: def forever(): while True: pass > Regular functions run to completion before returning control to the caller. This is true, but it need not be an inherent distinction of the function, but could be one made between the function, caller, and context: that is, the function can have points at which it can yield control, if it is called in the context of an event loop, it will yield at those points, and the caller can choose either to directly call the function and get sync behavior relative to itself or call it in the context of the same event loop as the caller is running, getting async behavior if the function yields early and the call returning an awaitable value (whether or not there is an early yield.) For the way many “scripting” languages are implemented, where there is already a kind of yield to the runtime at IO, this would be a pretty natural implementation and lets a caller treat any function as async or not without caring if it was specifically implemented as async or not, though you’ll grt no real concurrency in a single-threaded runtime without the function having yield points. (This is, AIUI, basically what the async implementation in Ruby does, which is why while there are libraries specifically designed for async use, existing Ruby async-unaware code that does IO can be called as async and works concurrently out of the box.)
- shadowgovt 2y agoI was speaking loosely and lumped "run forever with no progress" and "crash the program" into the same space; my apologies for the lack of clarity. A more precise way to say it is "regular functions either return or we're in a bad place (or we're in a very special circumstance, like 'this program burns cycles on purpose because the scheduler requires something to be running')." For async functions, failure to progress to a returnable state is expected behavior, so the whole system needs to be built around allowing computational progress while waiting for the state to change to where the return can occur.