Ask HN: Why can't await be used from non-async functions?
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. (Pretty sure this is full of errors so please correct me where I'm wrong)
Why can't non-async functions use await()?
I've also read that await() basically preempts the currently running function. How does this work?
Update: I'm re-reading http://journal.stuffwithstuff.com/2015/02/01/what-color-is-y... and I think the answer lies somewhere in this paragraph, but I can't wrap my head around it yet:
> The fundamental problem is “How do you pick up where you left off when an operation completes”? You’ve built up some big callstack and then you call some IO operation. For performance, that operation uses the operating system’s underlying asynchronous API. You cannot wait for it to complete because it won’t. You have to return all the way back to your language’s event loop and give the OS some time to spin before it will be done. Once operation completes, you need to resume what you were doing. The usual way a language “remembers where it is” is the callstack. That tracks all of the functions that are currently being invoked and where the instruction pointer is in each one. But to do async IO, you have to unwind and discard the entire C callstack. Kind of a Catch-22. You can do super fast IO, you just can’t do anything with the result! Every language that has async IO in its core—or in the case of JS, the browser’s event loop—copes with this in some way. [...]
I don't get the "You cannot wait for it to complete because it won’t." or the "But to do async IO, you have to unwind and discard the entire C callstack" parts.
I'm also using these resources, they help but I'm not there yet:
- https://stackoverflow.com/questions/47227550/using-await-ins...
90 comments
[ 3.1 ms ] story [ 150 ms ] threadWhen you call await the function doesn't necessarily have to be preempted (maybe the async function you called already returned) but if the async function hasn't returned then it has to be.
In most systems like that there is a scheduler that keeps a list of things that are being awaited on and keeps track of which ones are ready to return. When you call await on one function the scheduler looks at that list and chooses an await that is ready to continue and executes it.
Actually I don't understand why the `async` keyword is needed at all.
Synchronous I/O functions don't rerurn until the I/O is complete (or complete enough, anyway). The calling function effectively waits for completion.
Asynchronous I/O functions return right away, but completion is signaled elsewhere. The calling function does not have to wait (but it can choose to wait for completion if necessary). Asynchronous messaging may not even provide a completion signal, if you want to know it got there, the other side needs to send an asynchronous message back to confirm.
Await/async is confusing enough without redefining terms to mean their opposites.
At the top of the chain, this ultimately blocks the entire event loop (Javascript semantics are generally not concurrent), so no UI/network events can be processed until that promise resolves and the page/server is left non-responsive.
(And that's assuming you can somehow define clear semantics to run any Javascript code involved in resolving the promise; otherwise, you're deadlocked!)
Await actually pauses your function and allows the async runtime to work on other stuff, such as completing the function you just awaited on.
No, I'm not bitter.
Indeed:
Making a function support being paused and resumed requires changes in how the function is run and the data that is maintained while it is executing. In addition to behaving differently than sync functions when they execute, the results are also handled differently. Async functions always return a promise - whether they `return` a literal value, return another promise or throw an exception.
Due to these differences, it makes sense that the special nature of async functions and generators must be declared up-front, with the `async` keyword or `*` for generators. It would be possible to design the language such that the function type was determined by looking at whether it contains `yield` or `await` keywords. Python does this with generator functions. However this makes an important aspect of behavior less explicit.
1. Rewrite the async function to be a generator function with a wrapper that calls `next` on the generator whenever `await`-ed Promises resolve
2. Rewrite the generator function to be a big switch statement, together with a wrapper function that drives execution.
You can play around with generators and async functions in the TypeScript playground, with TSConfig set to target ES5, to see how this works: https://www.typescriptlang.org/play?target=1#code/GYVwdgxgLg...
Can you expand on what the actual differences are?
That starts to make sense to me: normal functions/subroutines will execute from start to finish, while these async functions (coroutines?) can be paused right in the middle of its execution. This is something fundamentally different then, which could explain why we need the `async` keyword, because those "asynchronous" functions are special.
I'd be interested to know the actual differences.
If that's true, does the "function coloring" problem apply to Promise-based code and callback-based code too? (Talking about JavaScript here)
https://hyperscript.org/
hyperscript does away with the distinction between sync and async code by moving it all to the runtime:
https://hyperscript.org/docs/#async
Why would it have to return a promise?
const a = async () => "hello world"
is the same as
const b = () => Promise.resolve("hello world")
Normal functions can't do that. If they wait for something they block, they can't give control back to the run loop.
And in a single-threaded environment, we _want_ this capability so that the loop is able to continue working despite the fact that the thing we're `await`ing for hasn't complete yet.
In the first case I am telling an executor what it has to do (it has to execute the thing then continue inside my function)
In the second case I am myself blocked into a state that will be unblocked when the thing returns.
That is in the first case my function must be of a type that can be stopped and continued and I need to save its state somewhere.
In the second case I don't need the ability to stop and continue my function.
That makes for two kinds of functions at some level. Low level languages will expose that. High level languages will hide it.
But there is a growing request for functions that can be both. I think that zig has them
And that state is saved in the heap, I presume? Can you provide a concrete example perhaps?
I get that the state in case of green threads (Goroutines for example) is saved in the call stack itself. Where all this is saved in async/await?
https://medium.com/front-end-weekly/callbacks-promises-and-a...
Careful, it doesn't work the same way in every single language that has these keywords.
> Why can't non-async functions use await()?
Because in Javascript at least, it only makes sense to await a promise, nothing more. A function marked as async will always return a promise. You can't await on anything that is not a promise.
I use this all the time in async functions with code branches, some of which await and some return an immediate value.
I understand that async functions return promises, but why can't you await for a promise from a non-async function? For me it would seem logical since by awaiting you essentially turn a promise into a concrete result which you can then use in your non-async code.
It's perfectly valid to await on a literal:
> async function test() {
}> let b = test()
> console.log(b)
> Promise { 'yep', [Symbol(async_id_symbol)]: 1215, [Symbol(trigger_async_id_symbol)]: 5, [Symbol(destroyed)]: { destroyed: false } }
The promise you create with `await a` resolves immediately (equivalent to Promise.resolve(a)), but it's still a promise.
I think what OP is assuming is a multi-threaded or multi-process environment, where the calling function can just block whatever execution context it's running in and wait until the async function returns.
The problem is that many environments are effectively non-multi-threaded, especially the (usually single) thread/queue/process that draws the UI and responds to user input. So if you block the UI thread, your whole app (at least from the standpoint of the user) stops responding.
Still, this should work in principle, but threads are more costly in terms of memory and context switching time than continuations, so it makes sense to allow the thread to continue to handle other tasks while waiting for e.g. I/O to complete.
This is what the author of the classic https://journal.stuffwithstuff.com/2015/02/01/what-color-is-... settles on as the best way of handling async tasks, but I recall there being some pushback on that here on HN.
> The problem is that many environments are effectively non-multi-threaded, especially the (usually single) thread/queue/process that draws the UI and responds to user input
Can you elaborate on why "especially" UI threads/queues/processes are "usually single"?
I don't think there's a particularly insightful answer to this question here, it's just that UI frameworks are almost universally written with the assumption that they are only used from one thread. UI frameworks can also use functionality spread across multiple components/libraries/systems, and making an UI kit thread-safe would likely require a lot of effort for dubious benefit (user input normally has to be processed strictly in order because clicking on a button and then pressing "enter" is different from pressing enter and then clicking on a button).
There are some specific scenarios where multiple threads are safe in a UI. For example, it's relatively common to be able to pass off an OpenGL context to another thread... so you can do OpenGL rendering in a thread separate from the main UI thread, if you like. Some UI frameworks specifically support this use case, e.g., certain methods on the OpenGL widgets are described as thread-safe. Individual OpenGL contexts are also not thread-safe and must be used from a single thread at a time (and they usually involve some thread-local context).
The typical way you make a responsive UI is by doing only UI work in the UI thread, and passing off all long-running computations to background threads.
In C#, you can't use the "await" keyword, but you can use the result of a Task<T> in a non-async function with ".Result" or ".GetAwaiter().GetResult()". https://stackoverflow.com/a/47648318/5107208
I don't know if it extends to other language implementations, but C# does not seem to have a reliable sync-over-async story.
https://blog.stephencleary.com/2012/07/dont-block-on-async-c...
https://devblogs.microsoft.com/pfxteam/await-and-ui-and-dead...
Well, I would say that it's the opposite of preemption. It's cooperative multitasking. Your async task is split into two tasks, with the await in the middle. When you call await, the first task finishes. The second task starts running once the await is done.
> Why can't non-async functions use await()?
In C#, they can. You can call `.RunSynchronously()` on a `Task`. C# supports lots of different TaskSchedulers that handle this differently.
C# is almost pathologically flexible here. Other languages typically assume that there is only one async task scheduler, and it's handled by the runtime. This task scheduler may be less flexible, and there are various design tradeoffs.
Incidentally, many high level programming languages _don't_ include GOTO, because people find it needlessly confusing and inhuman.
When I mark a function as `async`, I'm specifying that this function will be performing some form of asynchronous I/O. Any caller of that function (or its callers' callers) will therefore also be doing I/O. Such functions may take a while to "return" and are more likely to throw an exception.
Function coloring makes these properties explicit. By forcing all callers to also be `async`, it becomes easy to see which parts of the codebase are doing (somewhat risky) I/O work and which parts will be near-instant. No one ends up surprised when a call to `getFoo()` ends up getting `foo` from the network rather than the local object.
Similarly the absence of async doesn't guarantee the absence of IO, the function could be doing plain old sync IO or scheduling continuations on the event loop by hand.
IMHO there isn't any justification for async in "high overhead" languages, i.e. most of the dynamic languages that are not going to spawn tens if thousands of concurrent coroutines; those languages are better served by green threads or some sort of stackful continuations (the existence of async in python is particularly baffling).
It is more of a necessary evil in high performance languages were dedicating a call stack is a comparatively large cost. Even then I strongly believe that async functions should be awaited by default and any forking should be explicit.
YMMV.
To await means to wait for a Promise to resolve asynchronously. That's why async functions return a Promise instance.
Calling asyncFn() will immediately return a Promise instance. Then each await in the function waits for a Promise to resolve, before continuing to the next instruction. While that's happening, other code can keep running, like the last line in the example above.In contrast, a non-async function is synchronous, which means it's expected to run all instructions one after the other, then return a value. No other code can run while that's happening.
If await was allowed in a non-async function, it would have to block execution of all other code until the Promise is resolved/rejected.
This is how a synchronous HTTP request works: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequ...To avoid stopping the world, a non-async function can use Promise callbacks instead.
> async function run() {
}> run()
¹Not really, but close enough.
Originally, this was resolved via callbacks:
But that got very clunky, resulting in something called "callback hell" and the "callback pyramid of doom." So promises were introduced: But that's also clunky, so the async/await syntax was introduced: And that's actually quite pleasant to use.In JavaScript, you only have one thread. You really don't want to block it for any length of time. IO tends to take time. So IO is always async. There's no way to turn something async into something synchronous without blocking your thread waiting.
Other languages do let you block a thread to wait for an async task to complete, JavaScript doesn't mostly because there is only one thread.
> Given a single thread, how can we do multiple things at once (slow I/O, process user events like a mouse click etc.)
And the answer to this, is that we use continuations. Is that correct?
Note that you can call an async function from a non-async function. Assuming b() is async and a() and c() are not, a manual approach looks like this:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
Can it be done? sure:
can be converted to: the issue is that this is terribly inefficient as you are asking every 10ms whether or not the result is ready.One of the reasons this is so inefficient is that you have to spend time checking (which is wasteful if it isnt ready) and waiting. If you check more frequently, you're wasting CPU. If you spend more time waiting, you can waste up to that much time if the result is ready right after you go to sleep.
The alternative is to have the operating system tell you when its ready. This is done through the event loop. So this would turn the code transformation into something like the following:
but what do you return? you have to return a promise (i.e. a callback). Therefore, all callers of this function have to be ready to handle this.Not necessarily! In a typical eventloop system - like with libuv - events and readiness will be checked within the eventloop while no user code is executed. The eventloop runs something along:
Your whole promise code is part of `executeAllReadyCallbacks`. Thereby any blocking there will block fetching new readiness events, and thereby the code will be stuck.It would only work if the polling OS functions would run on a different thread - which some runtimes like .NET might actually do. However its very often avoided since its less efficient than polling events inline without synchronization, and in Javascript wouldn't work.
When you now use the c
When you write (Python):
write will, after several layers of abstractions and wrappers, look like this (on a reactor/readiness-oriented system): "yield" here means to pass a bit of data up to the event loop (neé coroutine scheduler), and that event loop will watch (using select/epoll) for that socket to become writable and then hand control back.Because this inversion of control doesn't happen unless you (usually) explicitly wrap a coroutine (e.g. asyncio.run_until_complete) with an event loop, you can't just call a coroutine. It doesn't know when to resume in these designs, and your synchronous function doesn't know how to, either.