Node.js - 超出最大调用堆栈大小

use*_*183 68 stack-overflow recursion callstack node.js

当我运行我的代码时,Node.js抛出"RangeError: Maximum call stack size exceeded"了太多递归调用引起的异常.我试图增加Node.js堆栈大小sudo node --stack-size=16000 app,但Node.js崩溃没有任何错误消息.当我在没有sudo的情况下再次运行时,Node.js打印出来'Segmentation fault: 11'.有没有可能在不删除递归调用的情况下解决这个问题?

谢谢

hei*_*nob 92

你应该将你的递归函数调用包装成一个

  • setTimeout,
  • setImmediate 要么
  • process.nextTick

函数给node.js清除堆栈的机会.如果你不这样做并且有许多循环而没有任何真正的异步函数调用,或者如果你不等待回调,那么你RangeError: Maximum call stack size exceeded将是不可避免的.

有很多关于"潜在异步循环"的文章.这是一个.

现在更多示例代码:

// ANTI-PATTERN
// THIS WILL CRASH

var condition = false, // potential means "maybe never"
    max = 1000000;

function potAsyncLoop( i, resume ) {
    if( i < max ) {
        if( condition ) { 
            someAsyncFunc( function( err, result ) { 
                potAsyncLoop( i+1, callback );
            });
        } else {
            // this will crash after some rounds with
            // "stack exceed", because control is never given back
            // to the browser 
            // -> no GC and browser "dead" ... "VERY BAD"
            potAsyncLoop( i+1, resume ); 
        }
    } else {
        resume();
    }
}
potAsyncLoop( 0, function() {
    // code after the loop
    ...
});
Run Code Online (Sandbox Code Playgroud)

这是正确的:

var condition = false, // potential means "maybe never"
    max = 1000000;

function potAsyncLoop( i, resume ) {
    if( i < max ) {
        if( condition ) { 
            someAsyncFunc( function( err, result ) { 
                potAsyncLoop( i+1, callback );
            });
        } else {
            // Now the browser gets the chance to clear the stack
            // after every round by getting the control back.
            // Afterwards the loop continues
            setTimeout( function() {
                potAsyncLoop( i+1, resume ); 
            }, 0 );
        }
    } else {
        resume();
    }
}
potAsyncLoop( 0, function() {
    // code after the loop
    ...
});
Run Code Online (Sandbox Code Playgroud)

现在你的循环可能会变得太慢,因为我们每轮松开一点时间(一次浏览器往返).但你不必setTimeout每回合都打电话.通常每1000次都可以这样做.但这可能因您的堆栈大小而异:

var condition = false, // potential means "maybe never"
    max = 1000000;

function potAsyncLoop( i, resume ) {
    if( i < max ) {
        if( condition ) { 
            someAsyncFunc( function( err, result ) { 
                potAsyncLoop( i+1, callback );
            });
        } else {
            if( i % 1000 === 0 ) {
                setTimeout( function() {
                    potAsyncLoop( i+1, resume ); 
                }, 0 );
            } else {
                potAsyncLoop( i+1, resume ); 
            }
        }
    } else {
        resume();
    }
}
potAsyncLoop( 0, function() {
    // code after the loop
    ...
});
Run Code Online (Sandbox Code Playgroud)

  • 你的答案中有一些好的和坏的点.我真的很喜欢你提到的setTimeout()等.但是没有必要使用setTimeout(fn,1),因为setTimeout(fn,0)非常好(所以我们不需要每个%1000 hack的setTimeout(fn,1)).它允许JavaScript VM清除堆栈,并立即恢复执行.在node.js中,process.nextTick()稍好一些,因为它允许node.js在让你的回调恢复之前做一些其他的事情(I/O IIRC). (6认同)
  • @ joonas.fi:我的"hack"与%1000是必要的.在**每个**循环上执行setImmediate/setTimeout(甚至为0)的速度要慢得多. (4认同)
  • 注意用英文翻译更新您的代码德语评论......?:)我明白但其他人可能不那么幸运. (3认同)
  • 我想说在这种情况下最好使用setImmediate而不是setTimeout。 (2认同)

use*_*183 21

我找到了一个脏的解决方案

/bin/bash -c "ulimit -s 65500; exec /usr/local/bin/node --stack-size=65500 /path/to/app.js"
Run Code Online (Sandbox Code Playgroud)

它只是增加了调用堆栈限制.我认为这不适合生产代码,但我需要它只运行一次的脚本.


Ang*_*ity 6

在某些语言中,这可以通过尾调用优化来解决,其中递归调用在引擎盖下转换为循环,因此不存在最大堆栈大小达到错误.

但是在javascript中,当前的引擎不支持这一点,可以预见新版本的Ecmascript 6语言.

Node.js有一些标志来启用ES6功能但尾部调用尚不可用.

因此,您可以重构代码以实现一种称为trampolining或重构的技术,以便将递归转换为循环.


Wer*_*ous 5

我有一个类似的问题。我在连续使用多个 Array.map() 时遇到了问题(一次大约 8 个地图),并且出现了 maximum_call_stack_exceeded 错误。我通过将地图更改为“for”循环来解决此问题

因此,如果您使用大量 map 调用,将它们更改为 for 循环可能会解决问题

编辑

只是为了清楚起见和可能不需要但最好知道的信息, using.map()会导致准备好数组(解析 getter 等)并缓存回调,并且还在内部保留数组的索引(因此回调提供了正确的索引/值)。这与每个嵌套调用堆叠在一起,并且在未嵌套时建议小心,因为.map()可以在第一个数组被垃圾收集之前调用下一个数组(如果有的话)。

拿这个例子:

var cb = *some callback function*
var arr1 , arr2 , arr3 = [*some large data set]
arr1.map(v => {
    *do something
})
cb(arr1)
arr2.map(v => {
    *do something // even though v is overwritten, and the first array
                  // has been passed through, it is still in memory
                  // because of the cached calls to the callback function
}) 
Run Code Online (Sandbox Code Playgroud)

如果我们将其更改为:

for(var|let|const v in|of arr1) {
    *do something
}
cb(arr1)
for(var|let|const v in|of arr2) {
    *do something  // Here there is not callback function to 
                   // store a reference for, and the array has 
                   // already been passed of (gone out of scope)
                   // so the garbage collector has an opportunity
                   // to remove the array if it runs low on memory
}
Run Code Online (Sandbox Code Playgroud)

我希望这是有道理的(我没有最好的语言表达方式)并帮助一些人防止我经历的头部挠头

如果有人感兴趣,这里还有一个性能测试比较 map 和 for 循环(不是我的工作)。

https://github.com/dg92/Performance-Analysis-JS

for 循环通常比 map 好,但不包括 reduce、filter 或 find