在JavaScript中异步迭代大规模数组,而不会触发超出的堆栈大小

时间:2015-02-22 05:08:52

标签: javascript node.js recursion stack-overflow async.js

我的环境是NodeJS,尽管这也可能是与网络相关的问题。我从数据库中获取了大量数据,我试图对其进行枚举。但是,为了论证,我可以说我有一个包含20,000个字符串的数组:

var y = 'strstrstrstrstrstrstrstrstrstr';
var x = [];
for(var i = 0; i < 20000; i++)
  x.push(y);

我希望异步枚举这个列表,让我们说使用async library,然后说,因为我非常谨慎,我甚至一次将枚举限制为5次迭代:

var allDone = function() { console.log('done!') };
require('async').eachLimit(x, 5, function(item, cb){
  ...
  someAsyncCall(.., cb);
}, allDone);

期望在上面同时迭代5个x项,最终所有20,000个项目将被迭代,控制台将打印完成!&#39;。实际发生的是:

Uncaught exception: [RangeError: Maximum call stack size exceeded]

此时我认为这必须是异步库的某种错误,所以我编写了我自己的eachLimit版本:

function eachLimit(data, limit, iterator, cb) {
    var consumed = 0;
    var consume;
    var finished = false;
    consume = function() {
        if(!finished && consumed >= data.length) {
            finished = true;
            cb();
        }else if(!finished) {
            return iterator(data[consumed++], consume);
        }
    };
    var concurrent = limit > data.length ? data.length : limit;
    for(var i = 0; i < concurrent; i++)
        consume();
}

有趣的是,这解决了我的问题。但是当我将我的实验从nodeJS移到Chrome时,即使使用我的解决方案,我仍然会收到超出的堆栈大小。

显然,我的方法不会像async中包含的eachLimit方法那样增加堆栈。但是,我仍然认为我的方法很糟糕,因为可能不是20k项目,但对于某些大小的数组,我仍然可以使用我的方法超过堆栈大小。我觉得我需要使用尾递归来设计某种解决这个问题的方法,但是我不确定v8是否会针对这种情况进行优化,或者如果问题可能会出现问题。

3 个答案:

答案 0 :(得分:2)

  

我觉得我需要使用尾递归设计某种解决方案来解决这个问题,但我不确定v8是否会针对这种情况进行优化,或者是否可能出现问题。

你正在使用的延续传递方式已经是尾递归(或者无论如何都接近)。问题是大多数JS引擎在这种情况下都倾向于进行堆栈溢出。

有两种主要方法可以解决此问题:

1)使用setTimeout强制代码为异步。

您的代码发生的是您在原始函数返回之前调用返回回调。在某些异步库中,这最终会导致堆栈溢出。一个简单的解决方法是强制回调仅在事件处理循环的下一次迭代中运行,方法是将其包装在setTimeout中。翻译

//Turns out this was actually "someSyncCall"...
someAsyncCall(.., cb);

someAsyncCall(..., function(){
    setTimeout(cb, 0)
});

这里的主要优点是这很简单。缺点是这会给你的循环增加一些延迟,因为实现了setTimeout,这样回调总会有一些非零延迟(即使你把它设置为零)。在服务器上,您可以使用nextTick(或类似的东西,忘记确切的名称)来做类似的事情。

也就是说,拥有大量顺序异步操作循环已经有点奇怪了。如果您的操作实际上都是异步的,那么由于网络延迟,它将需要数年才能完成。

2)使用trampolining处理同步代码。

100%避免stackoverflow的唯一方法是使用bona-fide while循环。使用promises,编写伪代码会更容易:

//vastly incomplete pseudocode
function loopStartingFrom(array, i){
    for(;i<array.length; i++){
        var x = run_next_item(i);
        if(is_promise(x)){
            return x.then(function(){
                loopStartingFrom(array, i+1)
            });
        }
    }
}

基本上,您在实际循环中运行循环,以某种方式检测您的某个迭代是立即返回还是推迟到异步计算。当事情立即返回时,你保持循环运行,当你最终获得真正的异步结果时,你停止循环并在异步迭代结果完成时恢复它。

使用蹦床的缺点是它有点复杂。也就是说,有一些异步库可以保证不会发生stackoverflow(通过使用我在引擎盖下提到的两个技巧之一)。

答案 1 :(得分:2)

为了防止堆栈溢出,您需要避免consume自我复制。你可以使用一个简单的标志来做到这一点:

function eachLimit(data, limit, iterator, cb) {
    var consumed = 0,
        running = 0,
        async = true;
    function consume() {
        running--;
        if (!async)
            return;
        while (running < limit && consumed < data.length) {
            async = false;
            running++;
            iterator(data[consumed++], consume);
            async = true;
        }
        if (running == 0)
            cb();
    }
    running++;
    consume();
}

答案 2 :(得分:1)

您是否考虑过使用承诺?他们应该解决不断增加的堆栈问题(你也可以使用promises,这是我书中的一大优点):

// Here, iterator() should take a single data value as input and return
// a promise for the asynchronous behavior (if it is asynchronous)
// or any value if it is synchronous
function eachLimit(data, limit, iterator) {
    return Promise(function (resolve, reject) {
        var i = 0;
        var failed = false;

        function handleFailure(error) {
            failed = true;
            reject(error);
        }

        function queueAction() {
            try {
                Promise.when(iterator(data[i]))
                .then(handleSuccess, handleFailure);
            } catch (error) {
                reject(error);
            }
        }

        function handleSuccess() {
            if (!failed) {
                if (i < data.length) {
                    queueAction();
                    i += 1;
                } else {
                    resolve();
                }
            }
        }

        for (; i < data.length && i < limit; i += 1) {
            queueAction();
        }
    });
}