2014-01-05 58 views
60

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

感谢

+2

为什么你首先需要如此深的递归? –

+0

请问,你可以发表一些代码吗? 'Segmentation fault:11'通常意味着节点中存在一个错误。 – vkurchatkin

+1

@丹Abramov:为什么深递归?如果您希望遍历数组或列表并对每个数据库执行异步操作(例如某些数据库操作),则这可能会成为问题。如果您使用异步操作中的回调移动到下一个项目,那么对于列表中的每个项目,至少会有一个额外的递归级别。 heinob提供的反模式可以阻止烟囱吹出。 –

回答

77

你应该换你的递归函数调用到

  • setTimeout
  • setImmediate
  • process.nextTick

功能给node.js的清除栈的机会。如果你不这样做,并且有很多循环没有任何真实异步函数调用,或者如果你不等待回调,你的RangeError: Maximum call stack size exceeded不可避免的

有很多关于“潜在的异步循环”的文章。 Here is one

现在一些示例代码:

这是正确的:

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 
    ... 
}); 

现在你的循环可能会变得太慢,因为我们失去每轮一点点的时间(一个浏览器往返)。但是你不必在每一轮都打电话给setTimeout。通常它是o.k.每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 
    ... 
}); 
+5

答案中有一些好的和坏的点。我真的很喜欢你提到的setTimeout()等人。但是没有必要使用setTimeout(fn,1),因为setTimeout(fn,0)非常好(所以我们不需要每过%1000 hack就设置setTimeout(fn,1))。它允许JavaScript VM清除堆栈,并立即恢复执行。在node.js中,process.nextTick()稍微好一些,因为它允许node.js在让你的回调继续之前做一些其他的事情(I/O IIRC)。 –

+1

你是对的。 0更好。修复。 – heinob

+1

我会说在这些情况下最好使用setImmediate而不是setTimeout。 – BaNz

5

在某些语言中,这可以用尾巴调用优化,其中递归调用引擎盖下转化为一个循环存在所以没有最大堆栈大小达到了错误来解决。

但在JavaScript中,当前引擎不支持此功能,因此可预见新版本的语言Ecmascript 6

Node.js有一些标志来启用ES6功能,但尾呼叫尚不可用。

因此,您可以重构代码以实现名为trampolining的技术,或者重构为transform recursion into a loop

+0

谢谢。我的递归调用不返回值,所以有什么办法可以调用函数,而不是等待结果? – user1518183

+0

并且它改变了一些数据的功能,比如数组,它做了什么功能,输入/输出是什么? –

19

我发现了一个肮脏的解决方案:

/bin/bash -c "ulimit -s 65500; exec /usr/local/bin/node --stack-size=65500 /path/to/app.js" 

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

+13

祝你好运! – heinob

1

如果您不希望实现自己的包装,你可以使用一个排队系统,例如:但是,这可能取决于你的筹码大小不同async.queuequeue

0

关于增加最大堆栈大小,在32位和64位机器上,V8的内存分配默认值分别为700 MB和1400 MB。在较新版本的V8中,64位系统的内存限制不再由V8设置,理论上没有限制。但是,运行Node的OS(操作系统)可以始终限制V8可以使用的内存量,因此任何给定进程的真正限制都不能一概而论。

尽管V8提供了--max_old_space_size选项,该选项允许控制进程可用的内存量,接受以MB为单位的值。如果您需要增加内存分配,只需在产生节点进程时将此选项传递给期望值即可。

减少给定节点实例的可用内存分配通常是一种很好的策略,特别是在运行多个实例时。与堆栈限制一样,考虑将大容量内存需求委派给专用存储层(如内存数据库或类似存储层)是否更好。

0

请检查您正在导入的功能和您在同一文件中声明的功能名称不同。

我会给你一个这个错误的例子。在(使用ES6)快递JS,考虑以下情形:

import {getAllCall} from '../../services/calls'; 

let getAllCall =() => { 
    return getAllCall().then(res => { 
     //do something here 
    }) 
} 
module.exports = { 
getAllCall 
} 

上述情况会造成臭名昭著的RangeError:最大调用堆栈大小超过错误,因为该功能保持自称这么多次,它用完最大调用堆栈。

大部分时间错误代码(如上面的那样)。其他解决方法是手动增加调用堆栈。那么,这适用于某些极端情况,但不建议。

希望我的回答对你有帮助。

相关问题