36

Threading-wise, what's the difference between web workers and functions declared as

async function xxx()
{
}

?

I am aware web workers are executed on separate threads, but what about async functions? Are such functions threaded in the same way as a function executed through setInterval is, or are they subject to yet another different kind of threading?

3
  • An async function is not a "parallel task", it's just syntactic sugar for promise chains. And it's threaded in the same normal way as those. Commented Mar 4, 2018 at 12:16
  • 2
    It's there in the keyword async - meaning "not occurring at the same time". Versus "parallel", which means "occurring or existing at the same time". Commented Mar 4, 2018 at 14:21
  • @Tersosauros One question here When we make a simple XHR call, FileReader, they does execute at the same time. Who do you think executes that part of computation if not main thread? When tasks of such evented calls are sitting in queues, then is there any computation happening? Say, file is being converted from raw data to other form of data. If yes, is there another thread operating on it? If not, then it is as good as sync code called but after a certain delay time. Commented Jun 18, 2021 at 12:58

6 Answers 6

25

async functions are just syntactic sugar around Promises and they are wrappers for callbacks.

// v await is just syntactic sugar
//                 v Promises are just wrappers
//                                         v functions taking callbacks are actually the source for the asynchronous behavior
   await new Promise(resolve => setTimeout(resolve));
 

Now a callback could be called back immediately by the code, e.g. if you .filter an array, or the engine could store the callback internally somewhere. Then, when a specific event occurs, it executes the callback. One could say that these are asynchronous callbacks, and those are usually the ones we wrap into Promises and await them.

To make sure that two callbacks do not run at the same time (which would make concurrent modifications possible, which causes a lot of trouble) whenever an event occurs the event does not get processed immediately, instead a Job (callback with arguments) gets placed into a Job Queue. Whenever the JavaScript Agent (= thread²) finishes execution of the current job, it looks into that queue for the next job to process¹.

Therefore one could say that an async function is just a way to express a continuous series of jobs.

 async function getPage() {
   // the first job starts fetching the webpage
   const response = await fetch("https://stackoverflow.com"); // callback gets registered under the hood somewhere, somewhen an event gets triggered
   // the second job starts parsing the content
   const result = await response.json(); // again, callback and event under the hood
   // the third job logs the result
   console.log(result);
}

// the same series of jobs can also be found here:
fetch("https://stackoverflow.com") // first job
   .then(response => response.json()) // second job / callback
   .then(result => console.log(result)); // third job / callback

Although two jobs cannot run in parallel on one agent (= thread), the job of one async function might run between the jobs of another. Therefore, two async functions can run concurrently.

Now who does produce these asynchronous events? That depends on what you are awaiting in the async function (or rather: what callback you registered). If it is a timer (setTimeout), an internal timer is set and the JS-thread continues with other jobs until the timer is done and then it executes the callback passed. Some of them, especially in the Node.js environment (fetch, fs.readFile) will start another thread internally. You only hand over some arguments and receive the results when the thread is done (through an event).

To get real parallelism, that is running two jobs at the same time, multiple agents are needed. WebWorkers are exactly that - agents. The code in the WebWorker therefore runs independently (has it's own job queues and executor).

Agents can communicate with each other via events, and you can react to those events with callbacks. For sure you can await actions from another agent too, if you wrap the callbacks into Promises:

const workerDone = new Promise(res => window.onmessage = res);

(async function(){
    const result = await workerDone;
        //...
})();

TL;DR:

JS  <---> callbacks / promises <--> internal Thread / Webworker

¹ There are other terms coined for this behavior, such as event loop / queue and others. The term Job is specified by ECMA262.

² How the engine implements agents is up to the engine, though as one agent may only execute one Job at a time, it very much makes sense to have one thread per agent.

Sign up to request clarification or add additional context in comments.

9 Comments

You may want to remove that "somewhat". This behaviour of async functions is exactly the same as with other functions doing asynchronous things.
Thanks, all answers are quite exhaustive. I am picking this one for its slightly better clarity concerning the comparison between how internal threads and webworker threads operate
Async functions can the best be converted to generator functions, just replace await by yield, and include some custom code that using the generator to call then on the returned value, and make those functions call either next(value) or throw(error), however, less people understand generator functions, compared to promises
@Ferrybig so there is no benefit at all :)
I still don't get it. Same thread or multiple threads, eventually main thread will receive a PONG from the non-main thread. It halts the execution in a very similar way as event loop will do. Why will we use Web workers? Some more practical use cases could have helped. "Real parallelism", sure, but if I am able to do the same thing with async nature of JS, why will I need a worker?
|
11

In contrast to WebWorkers, async functions are never guaranteed to be executed on a separate thread.

They just don't block the whole thread until their response arrives. You can think of them as being registered as waiting for a result, let other code execute and when their response comes through they get executed; hence the name asynchronous programming.

This is achieved through a message queue, which is a list of messages to be processed. Each message has an associated function which gets called in order to handle the message.

Doing this:

setTimeout(() => {
  console.log('foo')
}, 1000)

will simply add the callback function (that logs to the console) to the message queue. When it's 1000ms timer elapses, the message is popped from the message queue and executed.

While the timer is ticking, other code is free to execute. This is what gives the illusion of multithreading.

The setTimeout example above uses callbacks. Promises and async work the same way at a lower level — they piggyback on that message-queue concept, but are just syntactically different.

3 Comments

Not a downvoter. I find the use of the queue definitions to be odd though. MDN calls it a message queue, EcmaScript calls it a job queue, and w3 calls it a task queue. It would be nice if everyone could just agree on which terminology to use.
My answer is more or less an oversimplification. There are lots of different queues in JS. I've decided to focus on explaining the differences between adding some work to one of the queues, vs adding some work on a separate thread - which is what WebWorkers are all about.
Keep in mind that setTimeout will only fire when other code isn't running, e.g. it won't interrupt a while (1) loop.
8

Workers are also accessed by asynchronous code (i.e. Promises) however Workers are a solution to the CPU intensive tasks which would block the thread that the JS code is being run on; even if this CPU intensive function is invoked asynchronously.

So if you have a CPU intensive function like renderThread(duration) and if you do like

new Promise((v,x) => setTimeout(_ => (renderThread(500), v(1)),0)
    .then(v => console.log(v);
new Promise((v,x) => setTimeout(_ => (renderThread(100), v(2)),0)
    .then(v => console.log(v);

Even if second one takes less time to complete it will only be invoked after the first one releases the CPU thread. So we will get first 1 and then 2 on console.

However had these two function been run on separate Workers, then the outcome we expect would be 2 and 1 as then they could run concurrently and the second one finishes and returns a message earlier.

So for basic IO operations standard single threaded asynchronous code is very efficient and the need for Workers arises from need of using tasks which are CPU intensive and can be segmented (assigned to multiple Workers at once) such as FFT and whatnot.

6 Comments

"Workers are accessed by promises" - what?
@Bergi Yes let me be more explicit, just like evented code Workers run on a separate timeline and their resolutions (returened messages) can be accessed by Promises like new Promise((v,x) => firstWorker.onmessage(v), firstWorker.onmessageerror(x)).then(doSomething,reportError); in which case one may use Promise.all() to collect the results of segmented big task and concatenate them neatly.
@Redu One question here on "So for basic IO operations standard single threaded asynchronous code is very efficient and the need for Workers arises from need of using tasks which are CPU intensive". When we make a simple XHR call, FileReader, they does execute at the same time. Who do you think executes that part of computation if not main thread? When tasks of such evented calls are sitting in queues, then is there any computation happening? Say, file is being converted from raw data to other form of data. If yes, is there another thread operating on it?
@HalfWebDev Both an XHR call and accessing a file (if invoked with the asynchronous choice) have a lot of time to wait for a response to arrive even if they are fired one after the other. So i suppose nothing pretty much happens at the JS engine side since OS would deal with them possibly in a separate thread. At JS end, your async requests will remain in the event queue or in the microtask queue (for Promises) until a response arrives. Think it like when you click mouse it's not JS dealing with the mouse driver but i guess it's the browsers DOM (webkit etc) which is probably written in C++.
Regardless of being Async or Sync if your code is CPU intensive for the JS engine (the task is done in JS) then you should send it to a worker (a separate thread). If it is not CPU intensive (again... when the JS engine is concerned) you may just run it in the main thread. File access is a cheap async code for JS so you may fire as many as you like subsequently inside the main thread. Let the OS decide how to segment your file operations on separate threads.
|
6

I want to add my own answer to my question, with the understanding I gathered through all the other people's answers:

Ultimately, all but web workers, are glorified callbacks. Code in async functions, functions called through promises, functions called through setInterval and such - all get executed in the main thread with a mechanism akin to context switching. No parallelism exists at all.

True parallel execution with all its advantages and pitfalls, pertains to webworkers and webworkers alone.

(pity - I thought with "async functions" we finally got streamlined and "inline" threading)

Comments

5

Async functions have nothing to do with web workers or node child processes - unlike those, they are not a solution for parallel processing on multiple threads.

An async function is just1 syntactic sugar for a function returning a promise then() chain.

async function example() {
    await delay(1000);
    console.log("waited.");
}

is just the same as

function example() {
    return Promise.resolve(delay(1000)).then(() => {
        console.log("waited.");
    });
}

These two are virtually indistinguishable in their behaviour. The semantics of await or a specified in terms of promises, and every async function does return a promise for its result.

1: The syntactic sugar gets a bit more elaborate in the presence of control structures such as if/else or loops which are much harder to express as a linear promise chain, but it's still conceptually the same.

Are such functions threaded in the same way as a function executed through setInterval is?

Yes, the asynchronous parts of async functions run as (promise) callbacks on the standard event loop. The delay in the example above would implemented with the normal setTimeout - wrapped in a promise for easy consumption:

function delay(t) {
    return new Promise(resolve => {
        setTimeout(resolve, t);
    });
}

Comments

3

Here is a way to call standard functions as workers, enabling true parallelism. It's an unholy hack written in blood with help from satan, and probably there are a ton of browser quirks that can break it, but as far as I can tell it works.

[constraints: the function header has to be as simple as function f(a,b,c) and if there's any result, it has to go through a return statement]

function Async(func, params, callback)
{ 
 // ACQUIRE ORIGINAL FUNCTION'S CODE
 var text = func.toString(); 


 // EXTRACT ARGUMENTS
 var args = text.slice(text.indexOf("(") + 1, text.indexOf(")")); 
 args     = args.split(",");
 for(arg of args) arg = arg.trim();


 // ALTER FUNCTION'S CODE:
 // 1) DECLARE ARGUMENTS AS VARIABLES
 // 2) REPLACE RETURN STATEMENTS WITH THREAD POSTMESSAGE AND TERMINATION
 var body = text.slice(text.indexOf("{") + 1, text.lastIndexOf("}")); 
 for(var i = 0, c = params.length; i<c; i++) body = "var " + args[i] + " = " + JSON.stringify(params[i]) + ";" + body;
 body = body + " self.close();"; 
 body = body.replace(/return\s+([^;]*);/g, 'self.postMessage($1); self.close();');


 // CREATE THE WORKER FROM FUNCTION'S ALTERED CODE
 var code   = URL.createObjectURL(new Blob([body], {type:"text/javascript"}));
 var thread = new Worker(code);


 // WHEN THE WORKER SENDS BACK A RESULT, CALLBACK AND TERMINATE THE THREAD
 thread.onmessage =
 function(result)
 {
  if(callback) callback(result.data);

  thread.terminate();
 }

}

So, assuming you have this potentially cpu intensive function...

function HeavyWorkload(nx, ny) 
{
 var data = [];

 for(var x = 0; x < nx; x++)
 {
  data[x] = [];

  for(var y = 0; y < ny; y++)
  {
   data[x][y] = Math.random();
  }
 }

 return data;
}

...you can now call it like this:

Async(HeavyWorkload, [1000, 1000],
function(result)
{
 console.log(result);
}
);

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.