4 ms·
It's crazy that the programming community even accepted the concept of async/await as a sane one. Being sync or async is essentially a property of the attentio
by divan 3y ago
It's crazy that the programming community even accepted the concept of async/await as a sane one.
Being sync or async is essentially a property of the attention of the caller, not of the action itself. Is "eating a donut" a sync or async action? If I'm focusing all my attention on it, essentially putting all tasks aside (after) - then it's a synchronous action. If I'm reading a book/watching a video/walking/etc, while eating – it's an async action.
How does "func EatADonut() async {}" aka "eating a donut is an inherently async action" even make sense to people?
- paulddraper 3y agoIDK about donuts. But consider a network request. The vast majority of the time is not spent in the CPU. Right?
- divan 3y agoDoesn't matter, donut was just an abstraction. s/eat a donut/make a network request/ Is "network request" a synchronous or asynchronous activity? It depends whether your code blocks and wait's until response (or timeout) or continues executing and handling it when it comes. It's property of the "attention" of the caller, not of activity itself.
- joshuamorton 3y agoBut "can be gracefully executed asynchronously" is a property of an action. Something that pegs the CPU to 100% because it's doing intense processing isn't a good candidate for async. Similarly, some code can cause issues when written in a non-streaming fashion. Take the following example (in python): x = [x for x in range(1_000_000_000)] y = (x for x in range(1_000_000_000)) If you spawn a bunch of async workers doing the first one concurrently, you'll OOM your system. If you spawn a bunch of async workers doing the second one concurrently, your system will be fine. In other words, "async" is a label on a box of donuts that implies (though doesn't ensure, people can of course still do bad things) that the donut won't explode if you look away from it.
- divan 3y agoHmm, is it a post-factum rationalization or it's the original logic behind async/await? Let's mark "heavy" functions with a label, so user has to call them differently not to overload the system? Even if this is an original logic, why language is deciding for me what is considered heavy or not? What if I'm fully aware that I'm doing heavy processing and I want it to be happening in the background. What if I'm writing HFT software and every call is heavy for me. Language is not a right level of abstraction of marking "heaviness" of the code. It really just doesn't make sense. Why stop there and not start marking functions with how many times per second they can be called? Like you can call "light" function 100 trillion times per second and OOM the system, so let's mark it "func light() async 2_times_per_second {}". It only can be called from functions that have lower per second label. Plus, if Python cares so much about not OOM-ing the system I would start by not requiring 32-bit int to occupy 28 bytes in the first place.
- johnnyanmac 3y ago>is it a post-factum rationalization or it's the original logic behind async/await? Well, I'm not that deeply versed into any language's history, but I imagine any 15+ year old language had a point where they needed asyncrhonous functionality but had to write around legacy code. I don't think Javascript would have been written the way it was in the 90's if asynchronous operations were a mandatory priority. So it's probably a lot more "this is how we hack this in without creating Python 3.0". Relatively elegant to make it an optional part of the language that you explicitly choose to delve into, instead of a core feature that would have broken thousands of sites behind the scenes if you made it "right". >why language is deciding for me what is considered heavy or not? because we decided decades ago that we didn't like using fork(), nor creating/destroying processes ourselves. problems that would inevitably need multiple solutions when supporting multiple platforms because these aren't language features so much as OS calls made in a language that you may or may not be writing in. Remember at the end of the day that languages are abstractions on top of an OS. Any interesting ideas for problems involving more than a single process space that your OS makes for you is bound by what the OS's API lets you do. It's arbituary but also based on historical problems to wrangle. So we haven't had enough problems where we need to bound a function by how much it is called. We have for how to interact with all the above OS/language problems.
- wizofaus 3y agoIt's only functions that "block and wait" that it makes sense to be async - the point being that blocking and waiting doesn't use up CPU cycles, so you might as well free the thread up to do other stuff. If a function issues a socket write, then continues utilising the CPU while also periodically checking if data's available to read, it's effectively doing manually what async does for you, though I'm not sure there are too many real life examples of such functions. But pre-async it was certainly common enough for block-and-wait functions to tie up a thread and hence execution of programs with limited multi-threading (even today, GUIs often require all events and updates to be processed on the primary thread).
- kmac_ 3y agoHow about waiting until all or part of donut pack is eaten by multiple eaters? Jokes aside, async/await comes from easier handling of a callback code and automatic function splitting. Writing asynchronous donut eating in loops or with yielding of partial results is very easy to swallow with such syntactic sugar. The whole model is very easy to grasp and to work with. I'm not saying that's the best solution but it definitely works well. Regarding stickiness of function colors - it never happens when async/await is used correctly (in the same principle as IO monad isn't sticky).
- divan 3y agoDoesn't matter. You either wait until donut is eaten by others or you don't. Caller is deciding if it's "synchronous" (blocking) action or not. The question is how you translate mental models from real world to the code, and async/await fails here spectacularly. It's just weirdly unnatural thinking about sequencial processes. It requires tremendous amount of cognitive gymnastics just to reason about simple things.
- mplanchard 3y agoI’m sorry but isn’t this exactly how async/await works from the perspective of the caller? If you want to wait until the donut is eaten you `.await`. If you don’t, you do other stuff and then join the future later. The fact that the executor can get other stuff done while you’re awaiting the donut consumption is largely invisible from the caller’s perspective.
- divan 3y agoThat's only if you marked function with "async". Developer who writes code for "eating a donut" supposedly has better knowledge of how I'm supposed to eat it - synchonously or asynchronously. So if they didn't mark function as "async" I can't decide how I should eat it, it's decided for me.
- kmac_ 3y agoYou can eat it in the same thread. Some frameworks do that implicitly (even without await) . You can also control if it's consumed in particular thread or thread pool and decide which thread should continue. More, you can chain and/or parallelize it with other async and not-async functions without any effort. Additionally, you can handle errors and exceptions same as it's async and not-async function. Every good async/await framework offers that out of the box. More, some frameworks abstract executors and you can run the function remotely or even persist it with context. Of course you can achieve all that without async/await, but it makes that extremely easy.
- pmoleri 3y ago> How does "func EatADonut() async {}" aka "eating a donut is an inherently async action" even make sense to people? Of course it does, read it as "beware, something blocking down the road". If you can EatADonout without blocking, please do, but want it or not that's a different implementation, one that doesn't block and the signature it's telling you so. We're so used to sync and having hidden blocking operations. I wonder if in an alternate universe the first languages considered the blocking/async nature of operations and then some newer languages considered hiding this information into seemingly sync functions would produce similar but opposite outrage against it.
- divan 3y ago> want it or not that's a different implementation, Implementation is the same. In both cases it's the same set of CPU instructions, but async/await languages create artifical division, forcing developer to think othervise. So, let me explian my reasoning. Code starts with a developer's mental model of a problem and behavior of the system and then translating it into the code. The more straightforward this translation, the more readable the code. Code is a second degree map of problem domain so to speak ("real" world -> mental model -> code). Like if you want to add two values, the simplest form of code would be "add(2,2)" which is pretty straightforward. If the code forces you to do some mental gymnastics (i.e. "2 2 addOnlyEvenNumbers") - that's less straightforward, less clear and less readable. In the same vein, if you want to execute some function ("EatADonut" or "MakeHTTPCall") – you may care or not care about blocking and waiting for results. But it's your call. So it makes sense to give you two options to run this function. Go has simplest possible solution – "eatADonut()" vs "go eatADonut()". It doesn't matter what is a "default" here – it could be "eatADonut()" (go to background) vs "sync eatADount()". What matters that "eating a donut" is just a set of instructions to the CPU, and it's up to caller to decide how you want to execute it in terms of concurrency. Now, "async/await" approach turned this ownership of "synchronicity" around. Now function is deciding how it should be called. Mental model of "actions" now needs to be translated into "actions being async or sync for the purpose of fitting into this language concept". Which is cognitively expensive for no added benefit. Sure you can rationalize it, and get used to it as to any other absurd design, but it still adds unneeded complexity to the code, makes it less readable and less clear.
- bourneavent 3y ago>How does "func EatADonut() async {}" aka "eating a donut is an inherently async action" even make sense to people? You have to think of it from a higher level. Nobody knows if eatdonut is strictly async but everyone knows all IO calls are async. So something like socket.send would be async, this is obvious. Then anything that calls socket.send would then in turn become async itself. The async sort of spreads around like a virus. Any function that uses other async functions gets polluted with the async marker. So in this sense, you should think of it like this. If eatADonut is async it means it touches IO. It sends data somewhere else to some other thing. It also means it's not unit testable. In a nut shell This is the intuition you should have about async: Async Tells You about the properties of eatADonut. eatADonut doesn't tell you anything about whether or not it's async. This is largely identical to how the IO monad works in Haskell. Unless you want all your logic polluted with the IO monadic wrapper or the async marker you have to learn to segregate your functions into async functions and non-async functions. Under this intuition, EatADonut, by good design principles should NOT be async. This is what it should be: fn eatAdonut() -> ResultOfEatADonut async fn sendDataToIO(data: Data) -> () async parentCaller() -> () { let data = eatAdonut(); sendDataToIO(data.serialize()).await; () } The async marker forces you to organize your code in this way, You have to do this unless you want all your functions to be polluted by the async marker. This is the proper way to organize code Anyway, as it allows your code to be unit testable. Async calls are, in general, not unit testable because the functions are not pure. So if you look at what I did above, I segregated eatAdonut Away from IO and made part of the logic of the overall code testable via unit tests. IO calls should be very general and small functions while your other functions should be pure and unit testable. This is the overall intuition you should have about async. Believe it or not, this specific aspect of async is actually an improvement over golang style concurrency. Not an overall improvement, it's still debatable which style is better, but I'm saying the async marker is actually a feature that other styles of implementing concurrency don't offer. Basically, async encodes the concept of IO and function purity into the type system. It allows the type checker and You to tell which functions touch IO and which functions are pure and unit testable. People think unit testability is some kind of marginal benefit because you can cover testing with integration tests which are basically tests that happen across IO. It's more than that. The ultimate dream of programming something as if it's a bunch of modular lego blocks that you can elegantly place together is most fully realized with pure functions. Async enables more of this style of modularity in Rust (but not without some cost which is why it's debatable whether or not it's better than go style concurrency).
- bmacho 3y agoThis was my first (and obviously wrong) mental model of how async works. There are functions, you can call them sync or async if they handle IO or UI or they will get a necessary data later. I still don't understand why I can't fetch a URL from top level javascript. Also I don't understand why zig async and await passes the control flow that seemingly total arbitrary way. The naive approach (put async calls in a queue, and periodically check if they are completed or can be executed) seems fast, deterministic, and good enough in every way? Okay, maybe zig needs the speed, and can't just stop the execution of the sync code time to time to do something else, but why javascript? Maybe javascript async is build upon regular promises and regular objects, instead as a proper language element with proper support in the javascript engines? I don't know. Anyway async as is used with colors is totally against the picture that the words "async" and "await" suggest.
- rovolo 3y ago> I still don't understand why I can't fetch a URL from top level javascript. > Maybe javascript async is build upon regular promises and regular objects, instead as a proper language element with proper support in the javascript engines? Yes, they are promises. > The async and await keywords enable asynchronous, promise-based behavior to be written in a cleaner style, avoiding the need to explicitly configure promise chains. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe... > The naive approach (put async calls in a queue, and periodically check if they are completed or can be executed) seems fast, deterministic, and good enough in every way? Okay, maybe zig needs the speed, and can't just stop the execution of the sync code time to time to do something else, but why javascript? You're describing preemptive multitasking. You can create separate execution threads pretty easily in most languages (even super old C code), but that's not why Javascript and Zig have the 'async' keyword. The difficult part isn't the asynchronous execution; the hard part is handling the result from asynchronous code (i.e. the 'await' part). Promises are a simple mental model for organizing how pieces of code depend on the results of other pieces of code. The C# documentation does a good job explaining this: https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/ https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous... Note: From Zig's documentation it sounds like 'async' is cooperative multitasking. This is single-threaded execution. It is "concurrent" code, but it isn't executing in "parallel". Concurrency is difficult because it is explicitly resource management. You don't need concurrency for calculating the correct answer, you only need it for managing time.
- lmm 3y agoDoes blocking I/O or not is very much a property of the function itself, as is "may finish up running on a different thread from the one it started on" (or, if you prefer, "needs to be run under a runtime that provides that capability"). Sending a letter and getting a reply is an inherently async action; you can stare at the mailbox all day but you probably don't want to. Waiting in line at the bank is inherently sync; if you try to do something else and then come back they'll make you start over again.
- Too 3y agoYou can see the async keyword on a function definition as a hint to the caller, that the action will be IO bound rather than CPU bound. Caller can use this information to invoke 10000 IO bound tasks in parallel, whereas cpu bound tasks are limited to number of cores and may want to be throttled. In your analogy, I can eat a donut while simultaneously listening to music, but I can not eat a donut while simultaneously eating fish soup. Now where it get complicated is when a function does both io wait and heavy cpu processing. Now neither color of the function match. Forcing this upon the caller to invoke such functions in special ways is the strange part. Taken to the absurd you ought to have special syntax for every resource constraint in the computer, memory bound, disk bound, wifi bound, and so on. It would be better if the runtime could figure this out by itself and parallelize accordingly. The single threaded aspect of async functions does have certain other benefits, like not having to care as much about muxes when synchronizing state.
- kaba0 3y agoThere are so many nuances to function execution’s resource usages that I don’t see a point in putting boxes on that. The important part and the more intuitive human model for execution is from the POV of a single, calling thread. Whether it semantically makes sense to wait for the result of this call, or it doesn’t. If you need the result there is no going around that, you have to wait on that — it’s this function’s caller that has to decide what to do now. Other possible semantics would be to start multiple tasks and wait for them all — this is where structured concurrency comes in, giving a good analogue with gotos. Async-await is fundamentally an optimization, not about semantics only. Languages without runtimes simply can’t reason about it without language support, but managed ones could do so as every IO call goes through it — scheduling multiple tasks to a single core is now possible.
- Zababa 3y agoAsync/await, at least in JS, is here to make asynchronous code look like synchronous code, as in you can say "let toto = await asynchronousFunction(); doSomething(toto)". Without it when you want to do asynchronous stuff you either block on it (not good since JS is single-threaded), eg "let toto = asynchronousFunction(); doSomething(toto)" and nothing can be done during the time asynchronousFunction is waiting on IO or something (in a browser environment that means part of your website stops responding to user input); or pass as an argument to the asynchronous function a function that will be executed by the asynchronous function once it "wakes up" (usually called a "callback"), and while it's waiting, the JS runtime can execute other stuff, eg "asynchronousFunction((toto => doSomething(toto)))". Callbacks were I think from the start in JS. After a while promises were introduced, which compared to callbacks avoids nesting, and probably have some other advantages that I don't know about. Still, they are method chaining and not "regular code". For example regular try/catch won't work as usual, you have to use .catch(). Even later async/await got introduced, which allow you to write asynchronous code as if it was regular code, with a few exceptions (for example you can only use await in an async function, top-level await took a while to land on Node, stuff like that). I don't think using real world human actions helps with understanding any of that. Async functions make sense in JS because again, JS is single threaded and if you do a blocking call (like asking the kernel to read a file, or making an HTTP request and waiting for the response), the event loop is blocked. From Node.js documentation's on the synchronous function in the fs module: > The synchronous APIs perform all operations synchronously, blocking the event loop until the operation completes or fails.