9

Is using async and await the crude person's threads?

Many moons ago I learned how to do multithreaded Java code on Android. I recall I had to create threads, start threads, etc.

Now I'm learning Javascript, and I just learned about async and await.

For example:

async function isThisLikeTwoThreads() {
 const a = slowFunction();
 const b = fastFunction();
 console.log(await a, await b);
}

This looks way simpler than what I used to do and is a lot more intuitive.

slowFunction() would start first, then fastFunction() would start, and console.log() would wait until both functions resolved before logging - and slowFunction() and fastFunction() are potentially running at the same time. I expect it's ultimatly on the browser whether or not thesea are seperate threads. But it looks like it walks and talks like crude multithreading. Is it?

Will
  • 19,789
  • 10
  • 43
  • 45
  • 4
    _Javascript is a single threaded language_ – Nikita Madeev Jul 29 '20 at 12:18
  • 2
    Not really, no. The `async` and `await` stuff is basically just syntax sugar for constructing a generator-based mechanism for managing Promise instances. – Pointy Jul 29 '20 at 12:19
  • 3
    If `slowFunction()` and `fastFunction()` are asynchronous functions, it's assumed that they are suspended, waiting on a signal to re-enter the event loop. They are not CPU-bounded functions running in separate threads. – Patrick Roberts Jul 29 '20 at 12:20
  • @NikitaMadeev - That's a dramatic oversimplification. :-) – T.J. Crowder Jul 29 '20 at 12:37
  • 1
    @Pointy - You don't have to `await` just when doing a function call. In the OP's code above, for instance, if `slowFunction` and `fastFunction` both start asynchronous processes, the above starts both and allows them to run in parallel, then waits for them both to complete before doing the `console.log`. If the OP had used `const a = await longFunction(); const b = await slowFunction();`, they'd be done one after another instead; `fastFunction` wouldn't start its process until `slowFunction`'s finished. I've definitely done that in real code (not just with `Promise.all` :-) ). – T.J. Crowder Jul 29 '20 at 12:43
  • 1
    @T.J.Crowder well yea that's true I guess, it can be any expression and it behaves "promise-like" when the expression evaluates to a Promise. I don't get to use `async/await` in my environment so I try and keep it simple. – Pointy Jul 29 '20 at 12:46
  • 1
    Relevant question: https://stackoverflow.com/questions/34680985/what-is-the-difference-between-asynchronous-programming-and-multithreading – Will Jul 29 '20 at 13:48

3 Answers3

10

Is using async and await the crude person's threads?

No, not at all. It's just syntax sugar (really, really useful sugar) over using promises, which in turn is just a (really, really useful) formalized way to use callbacks. It's useful because you can wait asynchronously (without blocking the JavaScript main thread) for things that are, by nature, asynchronous (like HTTP requests).

If you need to use threads, use web workers, Node.js worker threads, or whatever multi-threading your environment provides. Per specification (nowadays), only a single thread at a time is allowed to work within a given JavaScript "realm" (very loosely: the global environment your code is running in and its associated objects, etc.) and so only a single thread at a time has access to the variables and such within that realm, but threads can cooperate via messaging (including transferring objects between them without making copies) and shared memory.

For example:

async function isThisLikeTwoThreads() {
 const a = slowFunction();
 const b = fastFunction();
 console.log(await a, await b);
}

Here's what that code does when isThisLikeTwoThreads is called:

  1. slowFunction is called synchronously and its return value is assigned to a.
  2. fastFunction is called synchronously and its return value is assigned to b.
  3. When isThisLikeTwoThreads reaches await a, it wraps a in a promise (as though you did Promise.resolve(a)) and returns a new promise (not that same one). Let's call the promise wrapped around a "aPromise" and the promise returned by the function "functionPromise".
  4. Later, when aPromise settles, if it was rejected functionPromise is rejected with the same rejection reason and the following steps are skipped; if it was fulfilled, the next step is done
  5. The code in isThisLikeTwoThreads continues by wrapping b in a promise (bPromise) and waiting for that to settle
  6. When bPromise settles, if it was rejected functionPromise is rejected with the same rejection reason; if it was fulfilled, the code in isThisLikeTwoThreads continues by logging the fulfillment values of aPromise and bPromise and then fulfilling functionPromise with the value undefined

All of the work above was done on the JavaScript thread where the call to isThisLikeTwoThreads was done, but it was spread out across multiple "jobs" (JavaScript terminology; the HTML spec calls them "tasks" and specifies a fair bit of detail for how they're handled on browsers). If slowFunction or fastFunction started an asynchronous process and returned a promise for that, that asynchronous process (for instance, an HTTP call the browser does) may have continued in parallel with the JavaScript thread while the JavaScript thread was doing other stuff or (if it was also JavaScript code on the main thread) may have competed for other work on the JavaScript thread (competed by adding jobs to the job queue and the thread processing them in a loop).

But using promises doesn't add threading. :-)

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • 1
    (FWIW, I go into the above in some detail in my new book. Promises are in Chapter 8, `async`/`await` in Chapter 9, and shared memory in Chapter 16. See my profile for links if you're interested.) – T.J. Crowder Jul 29 '20 at 12:48
  • nice answer @T.J. Crowder, is it safe to say that async/await is a model for concurrency in JavaScript, though it is not necessarily multi-threaded? - trying to wrap my head around it, thanks – phoenixdown Sep 21 '21 at 22:59
  • 1
    @phoenixdown - That's an interesting question. Yeah, I guess so. For instance, [in this fiddle](https://jsfiddle.net/tjcrowder/54kLf6se/), `a` and `b` are definitely running concurrently. So yes, I'd say [concurrency](https://en.wikipedia.org/wiki/Concurrent_computing) but **not** [parallelism](https://en.wikipedia.org/wiki/Parallel_computing), since although `a` and `b` overlap, they never do something in the same instant. (For that you need a worker thread.) Beware, though, that I don't have a formal computer science education. But to my understanding of the terms, yes, I think that's right. – T.J. Crowder Sep 22 '21 at 06:25
2

I suggest you read this to understand that the answer is no: https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop

To summarize, the runtime uses multiple threads for internal operations (network, disk... for a Node.js environment, rendering, indexedDB, network... for a browser environment) but the JavaScript code you wrote and the one you imported from different libraries will always execute in a single thread. Asynchronous operations will trigger callbacks which will be queued and executed one by one.

Basically what happens when executing this function:

async function isThisLikeTwoThreads() {
 const a = slowFunction();
 const b = fastFunction();
 console.log(await a, await b);
}

Execute slowFunction. Execute fastFunction. Enqueue the rest of the code (console.log(await a, await b)) when a promise and b promise have resolved. The console.log runs in the same thread after isThisLikeTwoThreads has returned, and after the possible enqueued callbacks have returned. Assuming slowFunction and fastFunction both return promises, this is an equivalent of:

function isThisLikeTwoThreads() {
 const a = slowFunction();
 const b = fastFunction();
 a.then(aResult => b.then(bResult => console.log(aResult, bResult)));
}
Guerric P
  • 30,447
  • 6
  • 48
  • 86
  • It's a little confusing to name your promises and your resolved values the same. And while it is _functionally_ equivalent to what you've written (other than forgetting to return the `Promise.all()` expression, and returning errors thrown synchronously by `slowFunction()` and `fastFunction()` as a rejected promise), it's actually a "strict" equivalent of: `return Promise.resolve(a).then(_a => Promise.resolve(b).then(_b => { console.log(_a, _b); }));` – Patrick Roberts Jul 29 '20 at 12:43
  • If `a` and `b` are native promise objects then that's correct, otherwise `Promise.resolve()` is not a no-op. I guess for the sake of simplicity it's close enough though, I just prefer not to use the word "strict" when it's not actually true. – Patrick Roberts Jul 29 '20 at 12:49
  • Yeah but if OP wrote `await` in front of them that supposes they return a `Promise`. I'll clarify it – Guerric P Jul 29 '20 at 12:50
  • `await` actually does insert an explicit `Promise.resolve()` call to convert a thenable into a promise ([step 2 of 6.2.3.1 `await`](https://tc39.es/ecma262/#await)) **edit** feel free to ignore this, just saw you removed "strict" – Patrick Roberts Jul 29 '20 at 12:57
  • I know it does, my goal was just to write equivalent simple code in case `a` and `b` contain native promises – Guerric P Jul 29 '20 at 13:01
0

Similar but not the same. Javascript will only ever be doing 'one' thing at a time. It is fundamentally single-threaded. That said, it can appear odd as results may appear to be arriving in different orders - exacerbated by functions such as async and await.

For example, even if you're rendering a background process at 70fps there are small gaps in rendering logic available to complete promises or receive event notifications -- it's during these moments promises are completed giving the illusion of multi-threading.

If you REALLY want to lock up a browser try this:

let a = 1;

while (a == 1){
    console.log("doing a thing");
}

You will never get javascript to work again and chrome or whatever will murder your script. The reason is when it enters that loop - nothing will touch it again, no events will be rendered and no promises will be delivered - hence, single threading.

If this was a true multithreaded environment you could break that loop from the outside by changing the variable to a new value.

Mark Taylor
  • 1,128
  • 8
  • 15