How to make non-blocking javascript code?
Asked Answered
E

9

50

How can I make a simple, non-block Javascript function call? For example:

  //begin the program
  console.log('begin');
  nonBlockingIncrement(10000000);
  console.log('do more stuff'); 

  //define the slow function; this would normally be a server call
  function nonBlockingIncrement(n){
    var i=0;
    while(i<n){
      i++;
    }
    console.log('0 incremented to '+i);
  }

outputs

"beginPage" 
"0 incremented to 10000000"
"do more stuff"

How can I form this simple loop to execute asynchronously and output the results via a callback function? The idea is to not block "do more stuff":

"beginPage" 
"do more stuff"
"0 incremented to 10000000"

I've tried following tutorials on callbacks and continuations, but they all seem to rely on external libraries or functions. None of them answer the question in a vacuum: how does one write Javascript code to be non-blocking!?


I have searched very hard for this answer before asking; please don't assume I didn't look. Everything I found is Node.js specific ([1], [2], [3], [4], [5]) or otherwise specific to other functions or libraries ([6], [7], [8], [9], [10], [11]), notably JQuery and setTimeout(). Please help me write non-blocking code using Javascript, not Javascript-written tools like JQuery and Node. Kindly reread the question before marking it as duplicate.

Ezar answered 28/10, 2014 at 18:36 Comment(6)
Effortlessly. You have to actually tell the thread to sleep for a duration in order to block the thread. To avoid sleeping, use timers with callbacks. sitepoint.com/settimeout-exampleHedgehog
There is no way to do this. Javascript is not multi-threaded and can only queue tasks. You can execute long running tasks at a later time, but not at the same time as other tasks.Imagism
@AndrewHoffman I'm not sure you understand. You can't tell JS to sleep, but you can keep it so busy that the UI loop can't service any events.Baker
You can block the thread with things like alert, which I wish every browser would disable. Bad programmers freezing my browser. -_-'Hedgehog
I believe I may have misunderstood the question having just realised that your "slow loop" was just an example. The answer I've given is the definitive way to break a long running computation into smaller pieces. However in the server call case, Promises are typically the right answer, and are now included in ES6. That said, any long-running async task API should provide a way to call a specific function on completion.Baker
Search mozilla developer network for fork() or exec() or pthread() and you will turn up empty. Why? Because support for child processes and threads is not a standard feature for browser javascript. Web workers is an experimental feature that is supposed to create additional processes that can communicate but do not share scope. Simultaneously running CPU code as you propose isn't supported. Practically all of the "async" JS code cited is about I/O events. On I/O: blah()Aforementioned
H
15

SetTimeout with callbacks is the way to go. Though, understand your function scopes are not the same as in C# or another multi-threaded environment.

Javascript does not wait for your function's callback to finish.

If you say:

function doThisThing(theseArgs) {
    setTimeout(function (theseArgs) { doThatOtherThing(theseArgs); }, 1000);
    alert('hello world');
}

Your alert will fire before the function you passed will.

The difference being that alert blocked the thread, but your callback did not.

Hedgehog answered 28/10, 2014 at 18:55 Comment(3)
I prefer the clarity of this answer over @Alnitak's. But, as @Baker pointed out, it's worth noting that one can also use setTimeout(..., 0) to avoid unnecessary waiting time. It's still non-blocking!Replacement
I agree, setTimeout(..., 0) helps to avoid unnecessary delays when the Event Call Stack is free.Kurtkurth
setTimeout(callback, 0) will print the same output without waiting for unwanted waiting time over here for testing purposes.Luciana
B
35

To make your loop non-blocking, you must break it into sections and allow the JS event processing loop to consume user events before carrying on to the next section.

The easiest way to achieve this is to do a certain amount of work, and then use setTimeout(..., 0) to queue the next chunk of work. Crucially, that queueing allows the JS event loop to process any events that have been queued in the meantime before going on to the next piece of work:

function yieldingLoop(count, chunksize, callback, finished) {
    var i = 0;
    (function chunk() {
        var end = Math.min(i + chunksize, count);
        for ( ; i < end; ++i) {
            callback.call(null, i);
        }
        if (i < count) {
            setTimeout(chunk, 0);
        } else {
            finished.call(null);
        }
    })();
}

with usage:

yieldingLoop(1000000, 1000, function(i) {
    // use i here
}, function() {
    // loop done here
});

See http://jsfiddle.net/alnitak/x3bwjjo6/ for a demo where the callback function just sets a variable to the current iteration count, and a separate setTimeout based loop polls the current value of that variable and updates the page with its value.

Baker answered 28/10, 2014 at 18:46 Comment(3)
Thank you for putting such work into your answer, but (like you mentioned in a comment) the for loop was just a dummy function to emulate something that takes a long time. Unless I misunderstand something, this code is only valid for that special case.Ezar
@Ezar oh well. The short answer is, you can't just write your three lines like you have and expect it to work - you have to call your long running task (asynchronously) and then arrange for another function to be called when that completes as I have done with the finished callback in my yieldingLoop example. The original program flow will carry on uninterrupted.Baker
Can you draw something on the screen during the execution?Ransome
H
15

SetTimeout with callbacks is the way to go. Though, understand your function scopes are not the same as in C# or another multi-threaded environment.

Javascript does not wait for your function's callback to finish.

If you say:

function doThisThing(theseArgs) {
    setTimeout(function (theseArgs) { doThatOtherThing(theseArgs); }, 1000);
    alert('hello world');
}

Your alert will fire before the function you passed will.

The difference being that alert blocked the thread, but your callback did not.

Hedgehog answered 28/10, 2014 at 18:55 Comment(3)
I prefer the clarity of this answer over @Alnitak's. But, as @Baker pointed out, it's worth noting that one can also use setTimeout(..., 0) to avoid unnecessary waiting time. It's still non-blocking!Replacement
I agree, setTimeout(..., 0) helps to avoid unnecessary delays when the Event Call Stack is free.Kurtkurth
setTimeout(callback, 0) will print the same output without waiting for unwanted waiting time over here for testing purposes.Luciana
Q
4

There are in general two ways to do this as far as I know. One is to use setTimeout (or requestAnimationFrame if you are doing this in a supporting environment). @Alnitak shown how to do this in another answer. Another way is to use a web worker to finish your blocking logic in a separate thread, so that the main UI thread is not blocked.

Using requestAnimationFrame or setTimeout:

//begin the program
console.log('begin');
nonBlockingIncrement(100, function (currentI, done) {
  if (done) {
    console.log('0 incremented to ' + currentI);
  }
});
console.log('do more stuff'); 

//define the slow function; this would normally be a server call
function nonBlockingIncrement(n, callback){
  var i = 0;
  
  function loop () {
    if (i < n) {
      i++;
      callback(i, false);
      (window.requestAnimationFrame || window.setTimeout)(loop);
    }
    else {
      callback(i, true);
    }
  }
  
  loop();
}

Using web worker:

/***** Your worker.js *****/
this.addEventListener('message', function (e) {
  var i = 0;

  while (i < e.data.target) {
    i++;
  }

  this.postMessage({
    done: true,
    currentI: i,
    caller: e.data.caller
  });
});



/***** Your main program *****/
//begin the program
console.log('begin');
nonBlockingIncrement(100, function (currentI, done) {
  if (done) {
    console.log('0 incremented to ' + currentI);
  }
});
console.log('do more stuff'); 

// Create web worker and callback register
var worker = new Worker('./worker.js'),
    callbacks = {};

worker.addEventListener('message', function (e) {
  callbacks[e.data.caller](e.data.currentI, e.data.done);
});

//define the slow function; this would normally be a server call
function nonBlockingIncrement(n, callback){
  const caller = 'nonBlockingIncrement';
  
  callbacks[caller] = callback;
  
  worker.postMessage({
    target: n,
    caller: caller
  });
}

You cannot run the web worker solution as it requires a separate worker.js file to host worker logic.

Quits answered 7/5, 2018 at 19:31 Comment(1)
Ryan, Can you please share links or explain what does callback(i,true) and callback(i,false) do? I searched but could not find exactly what we are calling here.Box
B
2

You cannot execute Two loops at the same time, remember that JS is single thread.

So, doing this will never work

function loopTest() {
    var test = 0
    for (var i; i<=100000000000, i++) {
        test +=1
    }
    return test
}

setTimeout(()=>{
    //This will block everything, so the second won't start until this loop ends
    console.log(loopTest()) 
}, 1)

setTimeout(()=>{
    console.log(loopTest())
}, 1)

If you want to achieve multi thread you have to use Web Workers, but they have to have a separated js file and you only can pass objects to them.

But, I've managed to use Web Workers without separated files by genering Blob files and i can pass them callback functions too.

//A fileless Web Worker
class ChildProcess {
     //@param {any} ags, Any kind of arguments that will be used in the callback, functions too
    constructor(...ags) {
        this.args = ags.map(a => (typeof a == 'function') ? {type:'fn', fn:a.toString()} : a)
    }

    //@param {function} cb, To be executed, the params must be the same number of passed in the constructor 
    async exec(cb) {
        var wk_string = this.worker.toString();
        wk_string = wk_string.substring(wk_string.indexOf('{') + 1, wk_string.lastIndexOf('}'));            
        var wk_link = window.URL.createObjectURL( new Blob([ wk_string ]) );
        var wk = new Worker(wk_link);

        wk.postMessage({ callback: cb.toString(), args: this.args });
 
        var resultado = await new Promise((next, error) => {
            wk.onmessage = e => (e.data && e.data.error) ? error(e.data.error) : next(e.data);
            wk.onerror = e => error(e.message);
        })

        wk.terminate(); window.URL.revokeObjectURL(wk_link);
        return resultado
    }

    worker() {
        onmessage = async function (e) {
            try {                
                var cb = new Function(`return ${e.data.callback}`)();
                var args = e.data.args.map(p => (p.type == 'fn') ? new Function(`return ${p.fn}`)() : p);

                try {
                    var result = await cb.apply(this, args); //If it is a promise or async function
                    return postMessage(result)

                } catch (e) { throw new Error(`CallbackError: ${e}`) }
            } catch (e) { postMessage({error: e.message}) }
        }
    }
}

setInterval(()=>{console.log('Not blocked code ' + Math.random())}, 1000)

console.log("starting blocking synchronous code in Worker")
console.time("\nblocked");

var proc = new ChildProcess(blockCpu, 43434234);

proc.exec(function(block, num) {
    //This will block for 10 sec, but 
    block(10000) //This blockCpu function is defined below
    return `\n\nbla bla ${num}\n` //Captured in the resolved promise
}).then(function (result){
    console.timeEnd("\nblocked")
    console.log("End of blocking code", result)
})
.catch(function(error) { console.log(error) })

//random blocking function
function blockCpu(ms) {
    var now = new Date().getTime();
    var result = 0
    while(true) {
        result += Math.random() * Math.random();
        if (new Date().getTime() > now +ms)
            return;
    }   
}
Briquet answered 14/12, 2017 at 6:1 Comment(0)
C
1

For very long tasks, a Web-Worker should be preferred, however for small-enough tasks (< a couple of seconds) or for when you can't move the task to a Worker (e.g because you needs to access the DOM or whatnot, Alnitak's solution of splitting the code in chunks is the way to go.

Nowadays, this can be rewritten in a cleaner way thanks to async/await syntax.
Also, instead of waiting for setTimeout() (which is delayed to at least 1ms in node-js and to 4ms everywhere after the 5th recursive call), it's better to use a MessageChannel.

So this gives us

const waitForNextTask = () => {
  const { port1, port2 } = waitForNextTask.channel ??= new MessageChannel();
  return new Promise( (res) => {
    port1.addEventListener("message", () => res(), { once: true } );
    port1.start();
    port2.postMessage("");
  } );
};

async function doSomethingSlow() {
  const chunk_size = 10000;
  // do something slow, like counting from 0 to Infinity
  for (let i = 0; i < Infinity; i++ ) {
    // we've done a full chunk, let the event-loop loop
    if( i % chunk_size === 0 ) {
      log.textContent = i; // just for demo, to check we're really doing something
      await waitForNextTask();
    }
  }
  console.log("Ah! Did it!");
}

console.log("starting my slow computation");
doSomethingSlow();
console.log("started my slow computation");
setTimeout(() => console.log("my slow computation is probably still running"), 5000);
<pre id="log"></pre>
Chevet answered 17/4, 2021 at 7:50 Comment(0)
T
1

Using ECMA async function it's very easy to write non-blocking async code, even if it performs CPU-bound operations. Let's do this on a typical academic task - Fibonacci calculation for the incredible huge value. All you need is to insert an operation that allows the event loop to be reached from time to time. Using this approach, you will never freeze the user interface or I/O.

Basic implementation:

const fibAsync = async (n) => {
  let lastTimeCalled = Date.now();

  let a = 1n,
    b = 1n,
    sum,
    i = n - 2;
  while (i-- > 0) {
    sum = a + b;
    a = b;
    b = sum;
    if (Date.now() - lastTimeCalled > 15) { // Do we need to poll the eventloop?
      lastTimeCalled = Date.now();
      await new Promise((resolve) => setTimeout(resolve, 0)); // do that
    }
  }
  return b;
};

And now we can use it (Live Demo):

let ticks = 0;

console.warn("Calulation started");

fibAsync(100000)
  .then((v) => console.log(`Ticks: ${ticks}\nResult: ${v}`), console.warn)
  .finally(() => {
    clearTimeout(timer);
  });

const timer = setInterval(
  () => console.log("timer tick - eventloop is not freezed", ticks++),
  0
);

As we can see, the timer is running normally, which indicates the event loop is not blocking.

I published an improved implementation of these helpers as antifreeze2 npm package. It uses setImmediate internally, so to get the maximum performance you need to import setImmediate polyfill for environments without native support.

Live Demo

import { antifreeze, isNeeded } from "antifreeze2";

const fibAsync = async (n) => {
  let a = 1n,
    b = 1n,
    sum,
    i = n - 2;
  while (i-- > 0) {
    sum = a + b;
    a = b;
    b = sum;
    if (isNeeded()) {
      await antifreeze();
    }
  }
  return b;
};
Threedimensional answered 11/2, 2022 at 18:39 Comment(0)
R
0

If you are using jQuery, I created a deferred implementation of Alnitak's answer

function deferredEach (arr, batchSize) {

    var deferred = $.Deferred();

    var index = 0;
    function chunk () {
        var lastIndex = Math.min(index + batchSize, arr.length);

        for(;index<lastIndex;index++){
            deferred.notify(index, arr[index]);
        }

        if (index >= arr.length) {
            deferred.resolve();
        } else {
            setTimeout(chunk, 0);
        }
    };

    setTimeout(chunk, 0);

    return deferred.promise();

}

Then you'll be able to use the returned promise to manage the progress and done callback:

var testArray =["Banana", "Orange", "Apple", "Mango"];
deferredEach(testArray, 2).progress(function(index, item){
    alert(item);
}).done(function(){
    alert("Done!");
})
Roumell answered 4/10, 2018 at 20:13 Comment(0)
L
0

I came across this thread while wondering the exact same thing.

How to I make my code non-blocking? How do you make the code execute in an order that is not the order of lines of code?

The Node.js docs contain the answer:

setTimeout instructs the CPU to store the instructions elsewhere on the bus, and instructs that the data is scheduled for pickup at a later time. Thousands of CPU cycles pass before the function hits again at the 0 millisecond mark, the CPU fetches the instructions from the bus and executes them.

So setTimeout is literally the language feature to make a block of code execute out of order.

To get to the output

beginPage
do more stuff
0 incremented to 10000000

we need to wrap the "slow part" in setTimeout.

function nonBlockingIncrement(n){
    setTimeout(
        function () {
            var i=0;
            while(i<n){
                i++;
            }
            console.log('0 incremented to '+i);
        },
        0
    )
}

console.log('begin');
nonBlockingIncrement(10000000);
console.log('do more stuff');

However, with this setup, the actual loop still runs in one go. Only the execution of the inner function is delayed.

If each cycle is expected to take long and you want to allow other code to run in between, you can change the loop so that each step schedules the next one. This is significantly slower though.

function nonBlockingIncrement(n){
    console.log('Starting incrementer.');
    var i = 0;

    function increment(i) {
        console.log('i=' + i);
        if (i < n) {
            i++
            setTimeout(() => increment(i), 0)
        } else {
            return
        }
    }

    increment(i)

    console.log('Incrementer done.');
}

console.log('Before.');
nonBlockingIncrement(10000);
console.log('After.');

I guess you could used chunking (as pointed out in Alnitak's answer) to combat the performance issues here. Instead of each step scheduling the next, you would process several steps (a chunk) and in the end of the chunk processing schedule the next chunk. And so on.

Lugansk answered 24/3 at 4:31 Comment(0)
J
-1

I managed to get an extremely short algorithm using functions. Here is an example:

let l=($,a,f,r)=>{f(r||0),$((r=a(r||0))||0)&&l($,a,f,r)};

l
  (i => i < 4, i => i+1, console.log) 

/*
output:
0
1
2
3
*/

I know this looks very complicated, so let me explain what is really going on here.

Here is a slightly simplified version of the l function.

let l_smpl = (a,b,c,d) => {c(d||0);d=b(d||0),a(d||0)&&l_smpl(a,b,c,d)||0}

First step in the loop, l_smpl calls your callback and passes in d - the index. If d is undefined, as it would be on the first call, it changes it to 0.

Next, it updates d by calling your updater function and setting d to the result. In our case, the updater function would add 1 to the index.

The next step checks if your condition is met by calling the first function and checking if the value is true meaning the loop is not done. If so, it calls the function again, or otherwise, it returns 0 to end the loop.

Judicious answered 19/1, 2023 at 0:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.