等待但从未解决/拒绝承诺内存使用[重复]

Posted

技术标签:

【中文标题】等待但从未解决/拒绝承诺内存使用[重复]【英文标题】:Awaited but never resolved/rejected promise memory usage [duplicate] 【发布时间】:2019-12-16 03:42:09 【问题描述】:

awaitPromise 既不解决也不拒绝(从不解决/未完成)会导致内存泄漏吗?

我在查看带有 slorber/awesome-debounce-promise 的 React hooks 时对此感到好奇,它创建了新的 Promise,但只解决了最后一个 Promise,因此留下了许多/最不稳定/未实现的内容。

【问题讨论】:

有趣的问题。我认为理论上不应该,因为它应该继续检查承诺是否解决,如果没有,请等待并再次检查。不过,结果可能会因目标而异,因为这取决于如何原生处理和/或最终填充 promise。 解除承诺不应导致它永远不会被解决/拒绝。只是你忽略了它们。所以它不应该影响记忆。例如,如果您创建了一个 Promise 并且从不费心等待或附加到它的 thenable 上,那么垃圾收集器只会清理它们。 @Keith 根据描述和使用示例,它似乎在等待创建的每个新承诺。 "每次调用都会返回一个promise", const result = await searchAPIDebounced(text);, "只有最后一次调用返回的promise会被解析"(它们也不会被拒绝)。跨度> 不,他们都会解决或拒绝。 debouce 所做的只是返回最后一个承诺。描述是误导性的。写得不好的承诺可能无法解决/拒绝,但这将是一个不同的问题.. 他在使用awesome-imperative-promise,所做的只是创建一个包装承诺,真正的物理承诺仍然会拒绝/解决。 【参考方案1】:

前言(你可能知道!):

await 是使用 promise 回调的语法糖。 (真的,真的,真的很好糖。) async 函数是 javascript 引擎为您构建承诺链等的函数。

答案:

相关的不是promise是否被解决,而是promise回调(以及它们引用/关闭的东西)是否保留在内存中。当 Promise 在内存中并且未解决时,它引用了它的回调函数,将它们保存在内存中。有两件事使这些引用消失了:

    兑现承诺,或 释放所有对 Promise 的引用,使其符合 GC 的条件(可能,更多内容见下文)

在正常情况下,promise 的使用者将处理程序与 promise 挂钩,然后要么根本不保留对它的引用,要么只在处理程序函数关闭的上下文中保留对它的引用,不在别处。 (例如,而不是将 promise 引用保存在长期存在的对象属性中。)

假设 debounce 实现释放了它对永远不会解决的 promise 的引用,并且 promise 的使用者没有在这个相互引用循环之外的某个地方存储引用,那么 promise 和注册到它的处理程序 (以及他们持有唯一引用的任何东西)都可以在对 Promise 的引用被释放后被垃圾回收。

这需要在实施方面相当小心。例如 (感谢 Keith 标记此问题),如果 promise 使用其他 API 的回调(例如,addEventListener)并且回调关闭对 promise 的引用,因为另一个 API 有对回调的引用,这可能会阻止对承诺的所有引用被释放,从而将承诺所引用的任何内容(例如它的回调)保留在内存中。

因此,这将取决于谨慎的实施,并在一定程度上取决于消费者。可以编写保留对 Promise 的引用的代码,从而导致内存泄漏,但在正常情况下,我不希望消费者这样做。

【讨论】:

await somePromise; 不保留对承诺的引用吗?即使我们在其他地方没有任何对承诺的参考。 @Qtax - No. await somePromise 基本上是 (这里有很多手) somePromise.then(() => /*...the code following the await through the end of the function */).catch(reject); 其中rejectreject 的隐含承诺async 函数返回。如果somePromise 确实有一个变量(不是返回承诺的函数的替代),那么问题是somePromise 中的引用会持续多久,但如果它是(例如)局部变量回调结束,不会阻止 GC。 @briosheje 任何事情都是如此,而不仅仅是承诺。释放对任何事物的引用使其符合 GC 条件 @Keith - 这是一个很好的观点。这是实现的一种子情况,不释放对它永远不会解决的承诺的引用,但我应该在上面明确指出一个有点隐藏的。 @briosheje 当然,我们每天都在学习 :)。垃圾收集的要点归结为区分“已使用”和“未使用”引用,并清理“未使用”引用,无论引用指向什么。 GC 将引用的概念视为围绕任何特定数据类型(在任何语言中)的抽象。例如,这就是为什么,正如上面基思所指出的,任何引用全局变量的东西都保留在内存中(因为全局变量总是被“使用”),无论是承诺还是其他。 (当然,我在简短的评论中推测了所有内容,但这就是要点:))【参考方案2】:

我使用以下结构做了一些测试:

function doesntSettle() 
    return new Promise(function(resolve, reject) 
        // Never settle the promise
    );


let awaited = 0;
let resolved = 0;

async function test() 
    awaited++;
    await doesntSettle();
    resolved++;


setInterval(() => 
    for (let i = 0; i < 100; ++i) 
        test();
    
, 1);

在这里实现:https://codesandbox.io/s/unsetteled-awaited-promise-memory-usage-u44oc

在 Google Chrome 中运行 just the result frame 显示开发工具内存选项卡中的内存使用量不断增加(但不在性能/JS 堆选项卡下),这表明存在泄漏。运行它但解决承诺并没有泄漏。

运行这个增加的内存使用量增加了 1-4MB/秒。停止它并运行 GC 并没有释放任何它。

【讨论】:

很有趣,对它进行测试做得很好。我认为这表明 V8 的 async 函数实现存在问题(它们相对较新)。我不认为我在the near-equivalent non-async version 上看到了它。我相当确定理论上它不应该泄漏(即使它是async 函数)。不过看起来确实像在 V8 中一样。 Firefox 的 SpiderMonkey 不会发生这种情况。 @T.J.Crowder 我在 Firefox 和 Edge 中都没有看到它。所以就像你说的那样,这可能是 V8 实现的错误/功能。有趣的是,在 Chrome 中,在开发工具的“性能”选项卡上跟踪的 JS 堆大小并没有增长,而且似乎很好地被 GC,没有超过 ~16MB。 我会继续做的。看起来和闻起来都像虫子。 :-D 刚刚在 Chrome v89 中对此进行了测试,现在它似乎像 Firefox 一样对这些未解决的承诺进行垃圾收集,因此不再有内存泄漏。

以上是关于等待但从未解决/拒绝承诺内存使用[重复]的主要内容,如果未能解决你的问题,请参考以下文章

使用 fetch 时拒绝承诺 [重复]

处理多个承诺拒绝[重复]

量角器异步/等待 UnhandledPromiseRejectionWarning:未处理的承诺拒绝

暂停功能,直到承诺解决[重复]

Javascript,如何等待多个承诺[重复]

稍后赶上承诺拒绝[重复]