Hacker News new | ask | show | jobs
by akubera 1569 days ago
Summary: Explicit 'async' functions avoid implicit messiness and allow control over which sections of code are running at one time.

> From what I understand, in JavaScript at least, putting `await foo()` inside an async function, splits the calling function in two, with the 2nd half being converted to a callback

I wouldn't say that's wrong, but it may be a slight over-simplification if your mental-model isn't complete. For example you can await from within a loop which would make function-splitting kind of a strange thing to think about. But yes, every `await` is a suspension point that will be resumed and the function carries on, so effectively the rest of the function becomes a callback, but let's call it a "continuation", which is the callback that continues from where we left off.

Of course it's implementation specific, but at a high level these continuations are basically normal functions which may return either the "incomplete" pair (new_awaitable, next_continuation) for `await new_awaitable` statements, or the "complete" return-value for `return` statements. (Maybe other things like exceptions, but let's not go there)

It's the event-loop/task-scheduler which keeps track of these callback/continuations: when one returns with a complete-value, the scheduler will go find the continuation that was awaiting it, and call that with the new value. If it returns a new (awaitable, continuation) object, the loop will start the awaitable and repeat until it returns a value, then resume the continuation with the value.

So you can think of await as "return to the event loop with awaitable and keep calling the callbacks until a final value is returned".

This is where the function coloring comes from: async functions need access to that event loop to keep returning to and continuing from, they know they're being called in a special way and return these special values for await/return that the event loop knows how to deal with; normal functions don't have access and just return values in the "normal" way.

Of course, to reap any benefit from this you have to have multiple tasks running "at the same time". To just await one thing which awaits one thing is not much different than a normal function call/stack. But when you're listening on sockets and fetching HTTP resources and making database queries and waiting for user responses, each executing simultaneously but on one thread (so no thread contention), that's when async shines. But note: they all must be talking to the same event loop which manages all these coroutines, running one at a time.

But what happens if "normal" functions could await?

Put another way: effectively de-color functions by making them all "async" and thus able to await. (Unless I'm misinterpreting the initial question)

From what I said above, all we have to do is change the implementation such that when a function returns, internally it actually this special "complete" variable type (with the real returned value inside). Then, with an event loop you get normal behavior and forward to whatever function had called it. Then you just add the awaits which return "incomplete" variables and you get waiting behavior.

This would work, BUT it introduces an overhead of going to the event loop checking if the return is "complete", finding the caller, and forwarding it for EVERY function call. Rather than the usual stack popping/register storing of standard languages. Optimizable? yeah probably, but it's not zero-cost, especially true in languages like Python which doesn't have an implicit event loop like JavaScript, so you're making huge changes to the implementation to begin with.

We could approach from the other side: returns stay the same, but the act of calling await creates an event loop or some object which does the "callback-the-callbacks" routine. I guess this would be the stop-the-world approach? This one function would keep looping through the callbacks until a final value reached. On second thought, this isn't different than a standard function call, because any internal `awaits` would setup their own loops and you always end up with a single returned value (or infinite loop). There's no way for multiple loops to running the continuations.

If you really don't like the keyword "async" before your function definition you could implement implicit async functions: just make any function in your language which includes an `await` implicitly become "async". But this only kicks the can down the road, as if you use await, that means you're returning continuations, and any callers need to await your value, and therefore your function is now implicitly colored.

And how do you implement the equivalent of

    async function() { return 42; }
maybe?

    function() { return 42; await; }

Implicit event loop.

Maybe this is the core of the question: for a language like JavaScript which already has an event loop, why can't we do

    function foo() { let x = await whatever(); return x; }
    console.log(foo());
The await in a non-async function could be implemented such that it goes out to the global event loop, "registers" itself as dependent on the Promise returned from "whatever()" then sit and wait for the event loop to return the value. This should sound familiar to you as the behavior of the standard async function / continuation business, but now we're trying to say that foo doesn't return a stream of continuations, (it just takes a long time to run).

While this may be doable, a problem arises if foo was called from within an async function. Part of "contract" of async programming is your function is run as usual between calls to await; at those points you return to the event loop and let other continuations run, but until the next await, shared/global variables will remain untouched because nobody else is running. If you called foo() which then starts running `whatever` on the event loop again any guarantees about ordering of events or state of variables may change unexpectedly.

1 comments

> Maybe this is the core of the question: for a language like JavaScript which already has an event loop, why can't we do

    function foo() { let x = await whatever(); return x; }
    console.log(foo());
Yes, that's one of my main questions. What would happen if we did this? It's my understanding now, after reading a lot of answers (yours included), that in JS land this would result in a deadlock, because the event loop would be waiting for something (whatever()) that would itself be waiting for something else.