使用 setTimeout 避免堆栈溢出

Posted

技术标签:

【中文标题】使用 setTimeout 避免堆栈溢出【英文标题】:Avoiding stack overflow by using setTimeout 【发布时间】:2016-07-12 23:39:38 【问题描述】:

我发现了以下问题here:

下面的递归代码如果数组会导致栈溢出 列表太大。你怎么能解决这个问题并仍然保留递归 模式?

答案是:

可以通过修改 nextListItem 函数如下:

var list = readHugeList();

var nextListItem = function() 
    var item = list.pop();

    if (item) 
        // process the list item...
        setTimeout( nextListItem, 0);
    
;

堆栈溢出被消除,因为事件循环处理 递归,而不是调用堆栈。当 nextListItem 运行时,如果 item 不是 null,超时函数(nextListItem)被推送到事件队列 并且函数退出,从而使调用堆栈清除。当。。。的时候 事件队列运行它的超时事件,下一个项目被处理并且一个 计时器设置为再次调用 nextListItem。因此,该方法是 从头到尾处理,无需直接递归调用,因此 无论迭代次数如何,调用堆栈都保持清晰。

谁能给我解释一下:

    这个用例是否实用 为什么长数组会导致堆栈溢出

【问题讨论】:

值得注意的是setTimeout(fn, 0)并没有按预期工作:在大多数浏览器中,最小延迟是4ms。 @lonesomeday,是的,我知道,谢谢。我最后问的问题呢? :) 对我来说,给出的解释很清楚。如果你不能掌握它,你可能要从the basics开始。 【参考方案1】:

这只是trampolines 的一个hacky 替代品,而trampolines 又只是TCO 的一个hacky 替代品。

当您在 javascript 中调用函数时,您会在调用堆栈中添加一个框架。该框架包含有关函数范围内的变量及其调用方式的信息。

在我们调用函数之前,调用栈是空的。

-------

如果我们调用函数foo,那么我们会在栈顶添加一个新帧。

| foo |
-------

foo 完成执行时,我们再次将帧从堆栈中弹出,再次将其留空。

现在,如果foo 反过来调用另一个函数bar,那么我们需要在foo 正在执行的同时将一个新帧添加到堆栈中。

| bar |
| foo |
-------

希望您可以看到,如果一个函数递归调用自身,它会不断将新帧添加到调用堆栈的顶部。

| ...          |
| nextListItem |
| nextListItem |
| nextListItem |
| nextListItem |
----------------

递归函数将不断添加帧,直到它们完成处理,或者它们超过调用堆栈的最大长度,从而导致溢出。

因为setTimeout 是一个异步操作,它不会阻塞你的函数,这意味着nextListItem 将被允许完成并且它的框架可以从调用堆栈中弹出——防止它增长。递归调用将改为使用event loop 处理。

这种模式有用吗? 调用堆栈的最大大小depends on your browser,但它可以低至 1130。如果您想使用递归函数,那么你就有可能破坏调用堆栈。

蹦床使用类似的技术,但不是将工作卸载到事件循环,而是返回一个调用下一​​次迭代的函数,然后可以使用 while 循环管理调用(这不会影响堆栈) .

var nextListItem = function() 
  var item = list.pop();

  if (item) 
    // process the list item...
    return nextListItem;
  
;

while(recur = recur()) 

【讨论】:

谢谢,那么调用堆栈长度真的只是受限于函数调用的数量,而不是每个函数的内存消耗吗? 是的,通常调用堆栈将用堆栈帧的 N 个插槽的固定大小数组表示。如果你尝试在插槽 N + 1 中放入一些东西,那么你会得到一个溢出。 多么有趣,谢谢)而且它也与堆栈/堆内存分配中的堆栈无关? 有a good MDN article 一起解释了堆栈、堆和事件循环。 是的。一般在编程中听到"a stack"是指堆栈数据结构的一个实例,听到"the stack"是指调用堆栈,在大多数编程语言中都有。【参考方案2】:

    通常不需要,但如果您决定需要递归链接相同的函数调用以获取长序列,这可能会派上用场。

    当为特定程序分配的堆栈内存量已被充分利用时,递归操作期间会发生堆栈溢出。递归遍历的足够长的数组可能会导致堆栈溢出。也许你不明白call stack 是如何工作的?

【讨论】:

【参考方案3】:

重复的for 循环对于发布的代码是最有效的。但是,如果您的实际代码不能适合使用 for 循环,那么还有另一种选择。

setTimeout 的使用取决于您对“实用”的看法,所以我们只列出事实,以便您自己决定。

setTimeout 强制浏览器通过操作淹没 CPU 以获得毫秒精度的计时。这可能是非常低效的。 使用setTimeout,每次迭代将花费4ms。仅 4 次迭代将花费时间来渲染整个帧。 250 次迭代,一秒钟过去了。

但是setTimeout 的替代方法可以帮助您在不使用 setTimeout 的情况下准确地实现您想要做的事情:the DeferStackJS library。如果你使用 DeferStackJS,那么你需要做的只是如下。

var list = readHugeList();

var nextListItem = function() 
    var item = list.pop();

    if (item) 
        // process the list item...
        DeferStack( nextListItem );
    
;

请强调,上面的 sn-p 代码是为了演示如何集成 DeferStackJS。 说实话,使用 DeferStackJS 或 Array.prototype.pop 对于这个特定的 sn-p 来说是非常不合适的的代码。相反,下面的代码会打败他们。

var list = readHugeList();

var nextListItem = function() 
    "use strict";
    var item, i = list.length;
    while (i--)  // Bounds check: as soon as i === -1, the while loop will stop.
                  // This is because i-- decrements i, returning the previous
                  // value of i
        item = list[i];
        if (!item) break; // break as soon as it finds a falsey item 
        // Place code here to be executed if it found a truthy value:

        // process the list item...
    
    if (i !== -1) 
        // Place code here to be executed if it found a falsey value:

    
;

提到 DeferStackJS 的唯一原因是因为我坚信回答论坛的人的第一责任是回答最初提出的问题。然后在他们回答之后,他们可以发表评论,然后猜测这个问题是为了这样问的。

【讨论】:

以上是关于使用 setTimeout 避免堆栈溢出的主要内容,如果未能解决你的问题,请参考以下文章

使用setTimeout函数解决栈溢出问题

调用 setTimeout 会清除调用堆栈吗?

调用 setTimeout 会清除调用堆栈吗?

如何在 C++ 中处理或避免堆栈溢出

避免包装 DLL 中的堆栈溢出

通过map文件了解堆栈分配(STM32MDK5)--避免堆栈溢出