ChatGPT解决这个技术问题 Extra ChatGPT

Using "await" inside non-async function

I have an async function that runs by a setInterval somewhere in my code. This function updates some cache in regular intervals.

I also have a different, synchronous function which needs to retrieve values - preferably from the cache, yet if it's a cache-miss, then from the data origins (I realize making IO operations in a synchronous manner is ill-advised, but lets assume this is required in this case).

My problem is I'd like the synchronous function to be able to wait for a value from the async one, but it's not possible to use the await keyword inside a non-async function:

function syncFunc(key) {
    if (!(key in cache)) {
        await updateCacheForKey([key]);
    }
}

async function updateCacheForKey(keys) {
    // updates cache for given keys
    ...
}

Now, this can be easily circumvented by extracting the logic inside updateCacheForKey into a new synchronous function, and calling this new function from both existing functions.

My question is why absolutely prevent this use case in the first place? My only guess is that it has to do with "idiot-proofing", since in most cases, waiting on an async function from a synchronous one is wrong. But am I wrong to think it has its valid use cases at times?

(I think this is possible in C# as well by using Task.Wait, though I might be confusing things here).

If you are using await inside a function, the function by definition is asynchronous. Why not just add async to syncFunc? Or are you saying you want syncFunc to block the thread while it waits?
Not certain what issue is with existing code?
Yes, I'd like syncFunc to block

T
T.J. Crowder

My problem is I'd like the synchronous function to be able to wait for a value from the async one...

They can't, because:

JavaScript works on the basis of a "job queue" processed by a thread, where jobs have run-to-completion semantics, and JavaScript doesn't really have asynchronous functions — even async functions are, under the covers, synchronous functions that return promises (details below)

The job queue (event loop) is conceptually quite simple: When something needs to be done (the initial execution of a script, an event handler callback, etc.), that work is put in the job queue. The thread servicing that job queue picks up the next pending job, runs it to completion, and then goes back for the next one. (It's more complicated than that, of course, but that's sufficient for our purposes.) So when a function gets called, it's called as part of the processing of a job, and jobs are always processed to completion before the next job can run.

Running to completion means that if the job called a function, that function has to return before the job is done. Jobs don't get suspended in the middle while the thread runs off to do something else. This makes code dramatically simpler to write correctly and reason about than if jobs could get suspended in the middle while something else happens. (Again it's more complicated than that, but again that's sufficient for our purposes here.)

So far so good. What's this about not really having asynchronous functions?!

Although we talk about "synchronous" vs. "asynchronous" functions, and even have an async keyword we can apply to functions, a function call is always synchronous in JavaScript. An async function is a function that synchronously returns a promise that the function's logic fulfills or rejects later, queuing callbacks the environment will call later.

Let's assume updateCacheForKey looks something like this:

async function updateCacheForKey(key) {
    const value = await fetch(/*...*/);
    cache[key] = value;
    return value;
}

What that's really doing, under the covers, is (very roughly, not literally) this:

function updateCacheForKey(key) {
    return fetch(/*...*/).then(result => {
        const value = result;
        cache[key] = value;
        return value;
    });
}

(I go into more detail on this in Chapter 9 of my recent book, JavaScript: The New Toys.)

It asks the browser to start the process of fetching the data, and registers a callback with it (via then) for the browser to call when the data comes back, and then it exits, returning the promise from then. The data isn't fetched yet, but updateCacheForKey is done. It has returned. It did its work synchronously.

Later, when the fetch completes, the browser queues a job to call that promise callback; when that job is picked up from the queue, the callback gets called, and its return value is used to resolve the promise then returned.

My question is why absolutely prevent this use case in the first place?

Let's see what that would look like:

The thread picks up a job and that job involves calling syncFunc, which calls updateCacheForKey. updateCacheForKey asks the browser to fetch the resource and returns its promise. Through the magic of this non-async await, we synchronously wait for that promise to be resolved, holding up the job. At some point, the browser's network code finishes retrieving the resource and queues a job to call the promise callback we registered in updateCacheForKey. Nothing happens, ever again. :-)

...because jobs have run-to-completion semantics, and the thread isn't allowed to pick up the next job until it completes the previous one. The thread isn't allowed to suspend the job that called syncFunc in the middle so it can go process the job that would resolve the promise.

That seems arbitrary, but again, the reason for it is that it makes it dramatically easier to write correct code and reason about what the code is doing.

But it does mean that a "synchronous" function can't wait for an "asynchronous" function to complete.

There's a lot of hand-waving of details and such above. If you want to get into the nitty-gritty of it, you can delve into the spec. Pack lots of provisions and warm clothes, you'll be some time. :-)

Jobs and Job Queues

Execution Contexts

Realms and Agents


I understand there's only a single thread, but why not let the code author decide whether they'd like to hog it or not? Again, as I mentioned, my problem is easy to circumvent, I'm simply asking why it's explicitly forbidden in the first place.
@crogs: Because they can't hog it. For the async operation to complete, it has to receive an event, which means that a job has to be queued to call the event handler and then that job has to be picked up from the job queue. It can't do that if the job running your synchronous function is blocked, because you can't run two jobs at the same time in the same environment (even if one of them is blocked).
What would then happen if syncFunc is calling updateCacheForKey? Would it just continue executing the statements below the async function call since it can't suspend the job? Btw thanks for your good answer!
@ErebosM - Yes, that's right. If it wants to see the result of updateCacheForKey, it needs to use then on the promise it returns. (async functions return promises.)
Damn! This is the wrong answer! (But it clearly explains why I can't do what I want to do.)
D
DavidRR

You can call an async function from within a non-async function via an Immediately Invoked Function Expression (IIFE):

(async () => await updateCacheForKey([key]))();

And as applied to your example:

function syncFunc(key) {
   if (!(key in cache)) {
      (async () => await updateCacheForKey([key]))();
   }
}

async function updateCacheForKey(keys) {
   // updates cache for given keys
   ...
}

This doesn't seem to wait until the function finishes! codesandbox.io/s/t6cwg?file=/index.ts
@Ayyappa it is your mistake.you put logging after promise is done. what you expect?
This just creates another async function and puts the await in there, then calls the outer async function without await. So the outer function will run till it reaches the await then return control to syncFunc. Then everything after await will get run after syncFunc finishes. You could have achieved something similar just by calling updateCacheForKey directly from runSync.
I've downvoted, as this is misleading without more explanation: updateCacheForKey() is not immediately invoked if it actually does anything async. In that case all that happens immediately is that a Promise is created (to be executed later). However if updateCacheForKey() is genuinely sync, not returning Promise or doing an await, then it will immediately invoke it. In that case this is quite a cool hack for getting round the restriction that only an async function can use await. (BTW, the called function can be sync sometimes and async sometimes!)
c
cyco130

Now, this can be easily circumvented by extracting the logic inside updateCacheForKey into a new synchronous function, and calling this new function from both existing functions.

T.J. Crowder explains the semantics of async functions in JavaScript perfectly. But in my opinion the paragraph above deserves more discussion. Depending on what updateCacheForKey does, it may not be possible to extract its logic into a synchronous function because, in JavaScript, some things can only be done asynchronously. For example there is no way to perform a network request and wait for its response synchronously. If updateCacheForKey relies on a server response, it can't be turned into a synchronous function.

It was true even before the advent of asynchronous functions and promises: XMLHttpRequest, for instance, gets a callback and calls it when the response is ready. There's no way of obtaining a response synchronously. Promises are just an abstraction layer on callbacks and asynchronous functions are just an abstraction layer on promises.

Now this could have been done differently. And it is in some environments:

In PHP, pretty much everything is synchronous. You send a request with curl and your script blocks until it gets a response.

Node.js has synchronous versions of its file system calls (readFileSync, writeFileSync etc.) which block until the operation completes.

Even plain old browser JavaScript has alert and friends (confirm, prompt) which block until the user dismisses the modal dialog.

This demonstrates that the designers of the JavaScript language could have opted for synchronous versions of XMLHttpRequest, fetch etc. Why didn't they?

[W]hy absolutely prevent this use case in the first place?

This is a design decision.

alert, for instance, prevents the user from interacting with the rest of the page because JavaScript is single threaded and the one and only thread of execution is blocked until the alert call completes. Therefore there's no way to execute event handlers, which means no way to become interactive. If there was a syncFetch function, it would block the user from doing anything until the network request completes, which can potentially take minutes, even hours or days.

This is clearly against the nature of the interactive environment we call the "web". alert was a mistake in retrospect and it should not be used except under very few circumstances.

The only alternative would be to allow multithreading in JavaScript which is notoriously difficult to write correct programs with. Are you having trouble wrapping your head around asynchronous functions? Try semaphores!


D
Dmitriy

It is possible to add a good old .then() to the async function and it will work.

Should consider though instead of doing that, changing your current regular function to async one, and all the way up the call stack until returned promise is not needed, i.e. there's no work to be done with the value returned from async function. In which case it actually CAN be called from a synchronous one.


D
Darren Cook

This shows how a function can be both sync and async, and how the Immediately Invoked Function Expression idiom is only immediate if the path through the function being called does synchronous things.

function test() {
    console.log('Test before');
    (async () => await print(0.3))();
    console.log('Test between');
    (async () => await print(0.7))();
    console.log('Test after');
}

async function print(v) {
    if(v<0.5)await sleep(5000);
    else console.log('No sleep')
    console.log(`Printing ${v}`);
}

function sleep(ms : number) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

test();

(Based off of Ayyappa's code in a comment to another answer.)

The console.log looks like this:

16:53:00.804 Test before
16:53:00.804 Test between
16:53:00.804 No sleep
16:53:00.805 Printing 0.7
16:53:00.805 Test after
16:53:05.805 Printing 0.3

If you change the 0.7 to 0.4 everything runs async:

17:05:14.185 Test before
17:05:14.186 Test between
17:05:14.186 Test after
17:05:19.186 Printing 0.3
17:05:19.187 Printing 0.4

And if you change both numbers to be over 0.5, everything runs sync, and no promises get created at all:

17:06:56.504 Test before
17:06:56.504 No sleep
17:06:56.505 Printing 0.6
17:06:56.505 Test between
17:06:56.505 No sleep
17:06:56.505 Printing 0.7
17:06:56.505 Test after

This does suggest an answer to the original question, though. You could have a function like this (disclaimer: untested nodeJS code):

const cache = {}

async getData(key, forceSync){
if(cache.hasOwnProperty(key))return cache[key]  //Runs sync

if(forceSync){  //Runs sync
  const value = fs.readFileSync(`${key}.txt`)
  cache[key] = value
  return value
  }

//If we reach here, the code will run async
const value = await fsPromises.readFile(`${key}.txt`)
cache[key] = value
return value
}