Search code examples
javascriptrecursioncallstackbusy-waiting

Busy-waiting alternative


I have a problem with a javascript-Function, where I can't use callback-Functions.

The function gets a List of "commands", and iterates over them. It performs one command after another. In order to do that, I have to use a recursive Function with callbacks.

Here's an example to describe it:

function processList( inputArray, cb ){
    if(inputArray.length == 0) return cb();  //No commands left

    var command = inputArray.pop();          //Get last Element from the List
    processCommand(command, function(){      //Execute the command
        return processList(inputArray, cb);  //Continue with the next command, when this one finished
    });
}

function processCommand(command, cb){
    setTimeout(function(){
        console.log("Processed command "+command);
        cb();
    }, 10);
}

This is how I call the function:

processList( ["cmd1", "cmd2", "cmd3"], function(){
    console.log("DONE");
});

It works perfectly, one command is executed after the previous one.

My Problem:

The list contains thousands of Elements, and it's even possible that the list gets new commands during processing. And I reach the maximum call-stack within seconds.

I don't need the call-stack. When I finished the last command, there are just thousands of returns, which guide me back to the function, which started everything.

I have no idea how to solve this problem, and I don't want to use busy waiting (which makes the code extremly inefficient).

Is there another trick? Like signals or trashing the call-stack?

Edit:

Here's a jsFiddle for demonstration: http://jsfiddle.net/db6J8/ (Notice, that your browser-tab might freeze/crash)

The error-Message is Uncaught RangeError: Maximum call stack size exceeded

I tested it with Chrome, you might have to increase the Array in other Browsers (IE has a huge Callstack).

Edit2:

Thanks for you help, I didn't recognise the difference between adding/removing the timeout. Hover, it doesn't solve my Problem. Here are some more details: I have different commands. Some commands are synchronous, others are asynchronous. So I have to use callback-Functions, but still have the problem with the callstack.

Here's an updated example:

var commands = [];
for(var i=15000; i>=0;i--){ commands.push(i); }
    processList(commands, function(){
    alert("DONE");
});


function processList( inputArray, cb ){
    if(inputArray.length == 0) return cb();  //No commands left

    var command = inputArray.pop();          //Get last Element from the List



    processCommand(command, function(){      //Execute the command
        return processList(inputArray, cb);  //Continue with the next command, when this one finished
    });
}

function processCommand(command, cb){
    if(command%2 == 0){     //This command is synchron
        console.log("Processed sync. command "+command);
        cb();
    }else{
        setTimeout(function(){
            console.log("Processed async. command "+command);
            cb();
        }, 1);
    }
}

And the fiddle: http://jsfiddle.net/B7APC/


Solution

  • // setTimeout(function(){
        console.log("Processed command " + command);
        cb();
    // }, 1);
    

    That's the reason. Without setTimeout, you easily will hit the call stack limit (Demo with named functions):

    Maximum recursion depth exceeded:
    finishedCommand _display/:16
    processCommand _display/:23
    processList _display/:15
    finishedCommand _display/:16
    processCommand _display/:23
    …
    processList _display/:15
    finishedCommand _display/:16
    processCommand _display/:23
    processList _display/:15
    <Global Scope> _display/:16
    

    Choose a synchronous loop instead of recursively calling processCommand.

    If you put the setTimeout back into effect, each timer event will call the function with a fresh new call stack, and it will never overflow:

    setTimeout(function later(){
        console.log("Processed command " + command);
        cb();
    }, 1);
    

    You see that the stack always looks like this when "DONE" is logged - regardless of how many commands you processed (Demo):

    BreakPoint at console.log()
    readyHandler show/:8
    processList show/:13
    finishedCommand show/:17
    later show/:24
    <Global Scope> // setTimeout