Promise.race会带来内存泄露

发布于 · 最后修改时间

假若有两个promise: a, b,现在它们都Promise.race([a,b])所包裹。
此时,如果a先完成了resolve,race也就有了返回值。
然而,b却迟迟没有被resolve或者reject……
结果会带来隐式的内存问题,就是a看上去被释放了,但其实没有。
v8论坛上有类似的bug提交:https://bugs.chromium.org/p/v8/issues/detail?id=9858

简单地模拟一下实现:

function race(...promises){
  return new Promise((resolve, reject)=>{
    for(const p of promises){
      p.then(resolve, reject)
    }
  })
}

我实际测试了一下,一些不同版本的v8似乎会有不同的表现,但终归是内存泄漏了,只是好像v12的是直接泄漏了捕捉不到,v13是能在内存堆栈里头看到。
我在工作的时候发现这个问题也是靠async_hooks的异步资源监控下才看到这个问题的存在。

解决方案其实也不难,核心问题就是消除引用:

  1. 首先我们需要一个Promise.prototype.safeThen的实现,因为Promise.prototype.then是会返回出一个新的promise的。做法其实就是只使用一次then来代理实现。其返回值就是thened: { resolves: Set<Function>, rejects: Set<Function>, isFinished: boolean }
  2. 接着我们基于safeThen来实现Promise.safePromiseRace,重点在于收集thened对象,并主动进行释放:
function safePromiseRace(...promises){
  return new Promise((resolve, reject)=>{
    const thenedList = []
    const finished = ()=>{
      thenedList.forEach(thened=>{
        thened.resolves.delete(safeResolve);
        thened.rejects.delete(safeReject);
      })
    }
    const safeResolve = (v)=>{resolve(v); finished();}
    const safeReject = (v)=>{reject(v); finished();}
    for(const p of promises){
      thenedList.push(p.safeThen(safeResolve, safeReject))
    }
  })
}