const fs = require("fs");
fs.readFile("aa.js", () => {
console.log("1");
process.nextTick(() => {
console.log("3");
});
});
fs.readFile("aa.js", () => {
console.log("2");
process.nextTick(() => {
console.log("4");
});
});
// the result is 1 3 2 4
const net = require("net");
const server = net.createServer(() => {}).listen(8080);
server.on("listening", () => {
console.log("1");
process.nextTick(() => {
console.log("3");
});
});
server.on("listening", () => {
console.log("2");
process.nextTick(() => {
console.log("4");
});
});
// the result is 1 2 3 4
IMO, these two async callback should behave the same,
but the result is different,
what's the reason behind the scene?
The first one is a race between two completely separate asynchronous fs.readFile() operations. Whichever one completes first is likely to get both of it's console logs before the other. Because these are operations that take some measurable amount of time and they both have to do the exact same amount of work, it's likely that one you started first will finish first and that's what you're seeing. But, technically, it's an indeterminate race between the two asynchronous operations and they could finish in any order. Since one is likely to finish slightly before the other, it's completion callback will be called before the other and it's also likely that the 2nd one won't yet be done before the next tick happens so that's why you see both log messages from whichever one finishes first.
Your second one is two event listeners for the exact same event. So, those two listeners are guaranteed to be called on the same tick one after the other. When an event listener object emits an event, it synchronously calls all the listeners for that event one after the other, all on the same tick. That's why you get 1 and then 2 before 3 and 4 which occur on the future ticks.
One should not confuse an eventEmitter object with the event queue. They are not the same thing. In this case, your server object is a subclass of an eventEmitter. Some code internal to the server decides to emit the listening event to listeners of the server's eventEmitter. That decision to emit the event was likely the result of some asynchronous operation that came from the event queue. But, to actually emit to the eventEmitter, this is just synchronous function calls to the registered listeners. The event queue is not involved in this. Internal to the eventEmitter code, it literally has a for loop that loops through the matching event handlers and calls each one, one after the other. That's why you get 1, then 2.
In fact, here's the code reference inside the .emit() method on the EventEmitter class definition that shows looping through the matching listeners and calling them synchronously. And, here's a snippet of that code calling each listener one after the other:
const len = handler.length;
const listeners = arrayClone(handler, len);
for (var i = 0; i < len; ++i)
Reflect.apply(listeners[i], this, args);
}
Related
I am not sure how statements in the global scope are placed into the JavaScript event queue. I first thought that the interpreter went through and added all global statements into the event queue line by line, then went and executed each event, but that logic does not line up with the example given below. How does the JavaScript interpreter add global statements to the event queue, and why is the output from the two examples given below different?
let handleResolved = (data) => {
console.log(data);
}
let p = new Promise((resolve, reject) => {
setTimeout(() => {resolve("1")}, 0)
});
p.then(handleResolved);
setTimeout(() => {
console.log("2");
}, 0);
The console output to the above code is
1
2
Now consider this example. Here, the difference is on the body of the promise callback, as there is a nested setTimeout
let handleResolved = (data) => {
console.log(data);
}
let p = new Promise((resolve, reject) => {
setTimeout(() = > {setTimeout(() => {resolve("1")}, 0)}, 0);
});
p.then(handleResolved);
setTimeout(() => {
console.log("2");
}, 0);
The console output to the above code is
2
1
What I don't understand is the order in which things are added to the event queue. The first snippet implies that the promise p will run, and then during its execution, resolve is put in the event queue. Once all of p's stack frames are popped, then resolve is run. After than p.then(...) is run, and finally the last console.log("2");
In the second example, somehow the number 2 is being printed to the console before the number 1. But would things not be added to the event queue in this order
1.) p
2.) setTimeout( () => {resolve("1")}, 0)
3.) resolve("1")
4.) p.then(...)
5.) console.log("2")
I clearly have some sort of event queue logic wrong in my head, but I have been reading everything I can and I am stuck. Any help with this is greatly appreciated.
There are several confusing things in your question that I think show some misconceptions about what is happening so let's cover those initially.
First, "statements" are not ever placed into the event queue. When an asynchronous task finishes running or when it is time for a timer to run, then something is inserted in the event queue. Nothing is in the queue before that. Right after you call setTimeout(), before the time has come for the setTimeout() to fire there is nothing in the event queue.
Instead, setTimeout() runs synchronously, configures a timer in the internals of the JS environment, associates the callback function you passed to setTimeout() to that timer and then immediately returns where JS execution continues on the next line of code. Sometime later when the time has been reached for the timer to fire and control has returned back to the event loop, the event loop will call the callback for that timer. The internals of exactly how this works vary a bit according to which Javascript environment it is, but they all have the same effect relative to other things going on in the JS environment. In nodejs, for example, nothing is ever actually inserted into the event queue itself. Instead, there are phases of the event loop (different things to check to see if there's something to run) and one of the phases is to check to see if the current time is at or after the time that the next timer event is scheduled for (the soonest timer that has been scheduled). In nodejs, timers are stored in a sorted linked list with the soonest timer at the head of the list. The event loop compares the current time with the timer on the timer at the head of the list to see if its time to execute that timer yet or not. If not, it goes about its business looking for other types of events in the various queues. If so, it grabs the callback associated with that timer and calls the callback.
Second, "events" are things that cause callback functions to get called and the code in that callback function is executed.
Calling a function that may then cause something to be inserted into the event queue, either immediately or later (depending upon the function). So, when setTimeout() is executed, it schedules a timer and some time later, it will cause the event loop to call the callback associated with that timer.
Third, there is not just a single event queue for every type of event. There are actually multiple queues and there are rules about what gets to run first if there are multiple different types of things waiting to run. For example, when a promise is resolved or rejected and thus has registered callbacks to call, those promise jobs get to run before timer related callbacks. Promises actually have their own separate queue for resolved or rejected promises waiting to call their appropriate callbacks.
Fourth, setTimeout(), even when given a 0 time, always calls its callback in some future tick of the event loop. It never runs synchronously or immediately. So, the rest of the current thread of Javascript execution always finishes running before a setTimeout() callback ever gets called. Promises also always call .then() or .catch() handlers after the current thread of execution finishes and control returns back to the event loop. Pending promise operations in the event queues always get to run before any pending timer events.
And to confuse things slightly, the Promise executor function (the callback fn you pass as in new Promise(fn)) does run synchronously. The event loop does not participate in running fn there. new Promise() is executed and that promise constructor immediately calls the executor callback function you passed to the promise constructor.
Now, lets look at your first code block:
let handleResolved = (data) => {
console.log(data);
}
let p = new Promise((resolve, reject) => {
setTimeout(() => {resolve("1")}, 0)
});
p.then(handleResolved);
setTimeout(() => {
console.log("2");
}, 0);
In order, here's what this does:
Assign a function to the handleResolved variable.
Call new Promise() which immediately and synchronously runs the promise executor callback you pass to it.
That executor callback, then calls setTimeout(fn, 0) which schedules a timer to run soon.
Assign the result of the new Promise() constructor to the p variable.
Execute p.then(handleResolved) which just registers handleResolved as a callback function for when the promise p is resolved.
Execute the second setTimeout() which schedules a timer to run soon.
Return control back to the event loop.
Shortly after returning control back to the event loop, the first timer you registered fires. Since it has the same execution time as the 2nd one you registered, the two timers will execute in the order they were originally registered. So, the first one calls its callback which calls resolve("1") to cause the promise p to change its state to be resolved. This schedules the .then() handlers for that promise by inserting a "job" into the promise queue.
That job will be run after the current stack frame finishes executing and returns control back to the system.
The call to resolve("1") finishes and control goes back to the event loop.
Because pending promise operations are served before pending timers, handleResolved(1) is called. That functions runs, outputs "1" to the console and then returns control back to the event loop.
The event loop then calls the callback associated with the remaining timer and "2" is output to the console.
What I don't understand is the order in which things are added to the event queue. The first snippet implies that the promise p will run, and then during its execution, resolve is put in the event queue. Once all of p's stack frames are popped, then resolve is run. After than p.then(...) is run, and finally the last console.log("2");
I can't really respond directly to this because this just isn't how things work at all. Promises don't "run". The new Promise() constructor is run. Promises themselves are just notification machines that notify registered listeners about changes in their state. resolve is not put in the event queue. resolve() is a function that gets called and changes the internal state of a promise when it gets called. p doesn't have stack frames. p.then() is run immediately, not later. It's just that all that p.then() does is register a callback so that callback can then be called later. Please see the above 1-11 steps for the sequence of how things work.
In the second example, somehow the number 2 is being printed to the console before the number 1. But would things not be added to the event queue in this order
In the second example, you have three calls to setTimeout() where the third one is nested inside the first one. This is what changes your timing relative to the first code block.
We have mostly the same steps as the first example except that instead of this:
setTimeout(() => {resolve("1")}, 0)
you have this:
setTimeout(() = > {setTimeout(() => {resolve("1")}, 0)}, 0);
This means that the promise constructor is called and this outer timer is set.
then, the rest of the synchronous code runs and the last timer in the code block is then set. Just like in the first code block, this first timer will get to call its callback before the second one. But, this time the first one just calls another setTimeout(fn, 0). Since timer callbacks are always executed in some future tick of the event loop (not immediately, even if the time is set to 0), that means that all the first timer does when it gets a chance to run is schedule another timer. Then, the last timer in the code block gets it's turn to run and you see the 2 in the console. Then, when that's done, the third timer (the one that was nested in the first timer) gets to run and you see the 1 in the console.
If we break down the second case so that each function is on its own we end up with
const handleResolved = (data) => {
console.log(data);
}
const promiseBody = (resolve, reject) => setTimeout( innerPromiseTimeout, 0, resolve );
const innerPromiseTimeout = (resolve) => setTimeout( resolveWith1, 0, resolve );
const resolveWith1 = (resolve) => resolve("1");
const timeoutLog2 = () => {
console.log("2");
};
// beginning of execution
// timers stack: [ ]
// promiseBody is executed synchronously
let p = new Promise( promiseBody );
// timers stack: [ innerPromiseTimeout ]
// this will happen only after resolveWith1 is called
p.then( handleResolved );
// timers stack: [ innerPromiseTimeout ]
setTimeout( timeoutLog2, 0 );
// timers stack: [ innerPromiseTimeout, timeoutLog2 ]
// some time later, innerPromiseTimeout is called
// timers stack: [ timeoutLog2, resolveWith1 ]
// timeoutLog2 is called
// timers stack: [ resolveWith1 ]
// resolveWith1 is called and then is executed in next microtask checkpoint
// timers stack: [ ]
Also note that setTimeout still has a minimum of 1ms in Chrome (they will soon remove it, but for the time being, it's there), so don't assume setTimeout(fn,0) will execute as the next task
I receive events on-the-fly in the correct order in which I want to process them after another (as well on-the-fly - so don't "store" them first).
The following example is not going to work as the function someEventOccured() could be called multiple times in a very close timespan. Thus, the execution of handleEvent would overlap in time (which I don't want). As I am writing to a file in handleEvent() I cannot have simultaneously execute the function and need to maintain the order...
async someEventOccured(data:string){
await handleEvent(data);
}
I have already tried using .then() and storing the promise, however I have no idea how to wait for the previous .then() to finish...
Your help is very appreciated <3
The general approach is to have a shared promise to queue onto:
let queue = Promise.resolve();
function someEventOccured(data:string) {
queue = queue.then(() => {
return handleEvent(data);
}).catch(err => {
// ensure `queue` is never rejected or it'll stop processing events
// …
});
}
Of course, this does implicitly store the data events in the closures on the promise chain.
My code:
async function run(){
process.nextTick(()=>{
console.log(1);
});
await (new Promise(resolve=>resolve()).then(()=>{console.log(2)}));
console.log(3);
process.nextTick(()=>{
console.log(4);
});
new Promise(resolve=>resolve()).then(()=>{console.log(5)});
}
run();
My expected output is the numbers to print 1,2,3,4,5 in order, but instead I get:
1
2
3
5
4
As expected, the first nextTick is evaluated before the first .then callback, because process.nextTick and .then are both deferred to future ticks, and process.nextTick is declared before .then. So 1 and 2 are outputted in order as expected.
The code should not reach what is after await until after .then is resolved, and this works as expected, as 3 is outputted in the expected place.
Then essentially we have a repeat of the first part of the code, but this time .then is called before process.nextTick.
This seems like inconsistent behavior. Why does process.nextTick get called before the .then callback the first time around but not the second?
The node.js event queue is not a single queue. It is actually a bunch of different queues and things like process.nextTick() and promise .then() handlers are not handled in the same queues. So, events of different types are not necessarily FIFO.
As such, if you have multiple things that go in the event queue around the same time and you want them served in a specific order, the simplest way to guarantee that order is to write your code to force the order you want, not to try to guess exactly how two things are going to get sequenced that went into the queue around the same time.
It is true that two operations of the exact same type like two process.nextTick() operations or two resolved promise operations will be processed in the order they were put into the event queue. But, operations of different types may not be processed in the order relative to each other that they were put in the event queue because different types of events are processed at different times in the cycle the event loop makes through all the different types of events.
It is probably possible to fully understand exactly how the event loop in node.js works for every type of event and predict exactly how two events that enter the event queue at about the same time will be processed relative to one another, but it is not easy. It is further complicated by the fact that it also depends upon where the event loop is in its current processing when the new events are added to the event queue.
As in my delivery example in my earlier comments, when exactly a new delivery will be processed relative to other deliveries depends upon where the delivery driver is when the new order arrives in the queue. The same can be true of the node.js event system. If a new event is inserted while node.js is processing a timer event, it may have a different relative order to other types of events than if it node.js was processing a file I/O completion event when it was inserted. So, because of this significant complication, I don't recommend trying to predict the execution order of asynchronous events of different types that are inserted into the event queue at about the same time.
And, I should add that native promises are plugged directly into the event loop implementation (as their own type of micro task) so a native promise implementation may behave differently in your original code than a non-native promise implementation. Again a reason not to try to forecast exactly how the event loop will schedule different types of events relative to one another.
If the order of processing is important to your code, then use code to enforce a specific completion processing order.
As an example of how it matters what the event queue is doing when events are inserted into the event queue, your code simplified to this:
async function run(){
process.nextTick(()=>{
console.log(1);
});
await Promise.resolve().then(()=>{console.log(2)});
console.log(3);
process.nextTick(()=>{
console.log(4);
});
Promise.resolve().then(()=>{console.log(5)});
}
run();
Generates this output:
1
2
3
5
4
But, simply change when the run() is called to be Promise.resolve().then(run) and the order is suddenly different:
async function run(){
process.nextTick(()=>{
console.log(1);
});
await Promise.resolve().then(()=>{console.log(2)});
console.log(3);
process.nextTick(()=>{
console.log(4);
});
Promise.resolve().then(()=>{console.log(5)});
}
Promise.resolve().then(run);
Generates this output which is quite different:
2
3
5
1
4
You can see that when the code is started from a resolved promise, then other resolved promises that happen in that code get processed before .nextTick() events which wasn't the case when the code was started from a different point in the event queue processing. This is the part that makes the event queue system very difficult to forecast.
So, if you're trying to guarantee a specific execution order, you have to either use all the same type of events and then they will execute in FIFO order relative to each other or you have to make your code enforce the execution order you want. So, if you really wanted to see this order:
1
2
3
4
5
You could use all promises which would essentially map to this:
async function run(){
Promise.resolve().then(() => {
console.log(1);
})
Promise.resolve().then(() => {
console.log(2)
});
await Promise.resolve().then(()=>{});
console.log(3);
Promise.resolve().then(() => {
console.log(4)
});
Promise.resolve().then(()=>{console.log(5)});
}
run();
Or, you change the structure of your code so the code makes it always process things in the desired order:
async function run(){
process.nextTick(async ()=>{
console.log(1);
await Promise.resolve().then(()=>{console.log(2)});
console.log(3);
process.nextTick(()=>{
console.log(4);
Promise.resolve().then(()=>{console.log(5)});
});
});
}
run();
Either of these last two scenarios will generate the output:
1
2
3
4
5
Thanks to the helpful comments that made me realize that not all javascript ways of deferring to a later tick are created equal. I had figured that (new Promise()).then, process.nextTick, even setTimeout(callback,0) would all be exactly the same but it turns out I can't assume that.
For now I'll leave here that the solution to my problem is simply to not use process.nextTick if it does not work in the expected order.
So I can change my code to (Disclaimer this is not actually good async code in general):
async function run(){
process.nextTick(()=>{
console.log(1);
});
await (new Promise(resolve=>resolve()).then(()=>{console.log(2)}));
console.log(3);
(new Promise(resolve=>resolve())).then(()=>{console.log(4)});
(new Promise(resolve=>resolve())).then(()=>{console.log(5)});
}
run();
Now I'm ensuring that 4 is logged before 5, by making both of them the same type of async call. Making both of them use process.nextTick would also ensure that 4 logs before 5.
async function run(){
process.nextTick(()=>{
console.log(1);
});
await (new Promise(resolve=>resolve()).then(()=>{console.log(2)}));
console.log(3);
process.nextTick(()=>{
console.log(4)
});
process.nextTick(()=>{
console.log(5)
});
}
run();
This is a solution to my problem, but if anyone wants to provide a more direct answer to my question I'll be happy to accept.
Let's consider this code:
async function testFunction() {
// event loop 1 - before any "await", we are still in original event loop, right?
const result = await Promise.resolve('abc');
// event loop 2 - we have a new event loop
return result;
}
// event loop 1
testFunction().then(result => {
// event loop 2? Or new event loop?
})
Does event loop changes again after async function done all asynchronous operations? Is the event loop right before return statement the same as in the then statement?
And is there an easy way to check it?
I'm working with some synchronous API that needs to be executed in the same event loop and I want to be sure it will work.
EDIT:
To make it more clear, this is the real life example:
async function dbTransaction(tableName, transactionMode) {
const db = await _dbPromise;
return db.transaction([tableName], transactionMode).objectStore(tableName);
}
Can I use the transaction returned by this asynchronous function?
Some related MDN info:
https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB#Adding_data_to_the_database
And important quote:
Now that you have a transaction you need to understand its lifetime.
Transactions are tied very closely to the event loop. If you make a
transaction and return to the event loop without using it then the
transaction will become inactive.
EDIT 2:
One more thing - the answer is yes - the transaction is usable after const objectStore = await dbTransaction(DB_IMAGE, DB_OP_READWRITE). Only if I await something else I get TRANSACTION_INACTIVE_ERR error - which makes sense.
So is it by design? Does it mean it is indeed in the same event loop iteration?
A transaction is only valid on a given stack.
When some asynchronous work is done, i.e. the browser gets a handler to indexedDB here:
const db = await _dbPromise;
a new stack is created and it continues till the end of that function.
But apart from the main stack, there's also a microtask stack and on that stack go all callbacks from promises and observers. If a promise is marked as resolved while executing instructions from a given stack, it is immediately called after the main stack empties and a transaction is still available there. (You can think of stack + microtasks as one 'tick' of a loop after which javascript passes control back to the event loop that decides what to do next).
So in your example:
async function testFunction() {
// tick 1 synchronous
const result = await Promise.resolve('abc');
// tick 1 microtask 1 (up resolved through the execution of the stack)
return result;
}
// tick 1
testFunction().then(result => {
// tick 1 microtask 2 (resolved through the execution of microtask 1)
})
You can get some info by checking performance panel:
This video might clear what an event loop is and how it works: https://vimeo.com/254947206
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?
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.
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.
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.
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);
});
}
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)
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);
}
);