You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JavaScript:新异步函数启动时终止旧异步线程方案问询

问题:终止旧异步函数以避免WebCodec资源冲突

我有多个可被用户随时调用的异步函数,但必须保证调用新函数时,所有之前运行的函数及其衍生的异步操作都被终止——否则它们会争抢同一个WebCodec Decoder资源,而该资源不支持多实例并发。

现有尝试与痛点

目前我用了一个全局计数器,所有函数都能访问:每个函数启动时递增并复制计数器,每次调用异步函数都传入这个计数器副本,在子例程启动和返回后检查全局计数器是否变化。但这种方式有明显问题:

  • 嵌套异步调用多的时候,需要把计数器副本层层传递,维护成本极高
  • 对非自行编写的内部异步函数完全无效

我希望实现类似下面的逻辑,有点像cancelAnimationFrame的机制,但能适配任意函数:

let functionCurrentlyRun = null

async function runFunction(f, args) {
  if (functionCurrentlyRun) {
    stopFunctionAndAllSubthreads(functionCurrentlyRun);
  }
  return await runAndSaveIn(f, args, functionCurrentlyRun) 
}

async function f1(args) {
  return await someAsyncCalls();
}

async function f2(args) {
  return await someAsyncCalls();
}

runFunction(f1, 42);
runFunction(f2, 43);

基于AbortController的尝试

之后我参考相关方案写了下面的代码,把所有await foo替换成await makeMeAbortIfNeeded(foo, mainSignal)后能正常运行,但还有两个问题:

  1. 不知道如何把AbortController重置为未终止状态
  2. 每次都要手动包裹所有await操作,太繁琐
<!DOCTYPE html>
<body>
  Hello <button id="buttonstart">Start me</button> <button id="buttonstop">Stop me</button>.
    <script type="text/javascript">
      const wait = (n) => new Promise((resolve) => setTimeout(resolve, n));

      const controller = new AbortController();
      const mainSignal = controller.signal;

      document.getElementById("buttonstart").addEventListener("click", async () => {
        console.log("Should be very first line");
        setTimeout(() => console.log("First timeout"));
        var x = await makeMeAbortIfNeeded(test3(), mainSignal);
        console.log("Last line of the main loop. I received:", x);
      })

      
      document.getElementById("buttonstop").addEventListener("click", () => {
        console.log("Click!");
        controller.abort();
      })

      function makeMeAbortIfNeeded(promise, signal) {
        return new Promise((resolve, reject) =>{
          // 如果信号已终止,立即抛出错误以拒绝Promise
          if (signal.aborted) {
            reject(signal.reason);
          }
          const myListener = () => {
            console.log("Just received a signal to abort");
            // 终止主操作并以终止原因拒绝Promise
            // 注意:如果Promise内部包含非阻塞代码,它仍会继续运行
            reject(signal.reason);
          };
          promise.then(x => {
            signal.removeEventListener("abort", myListener);
            resolve(x);
          });

          signal.addEventListener("abort", myListener);
        });
      }

      async function test3() {
        console.log("[test3] A");
        // 注意:所有await调用都需要用这个方法包裹
        await makeMeAbortIfNeeded(wait(3000), mainSignal);
        // 如果直接用 await wait(3000);
        // 那么即使test3被包裹,下面的日志还是会打印出来
        console.log("[test3] B");
        return 42
      }
    </script>
  </body>
</html>

解决方案

1. 解决AbortController无法重置的问题

AbortController一旦调用abort()就无法复用,正确的做法是每次启动新函数时创建新的AbortController实例,同时保留对旧控制器的引用,启动新函数前先调用旧控制器的abort():

let currentController = null;

async function runFunction(f, ...args) {
  // 终止之前的所有异步操作
  if (currentController) {
    currentController.abort("新函数启动,终止旧操作");
  }
  // 创建新的控制器
  const newController = new AbortController();
  currentController = newController;
  try {
    // 将信号传入目标函数,让函数内部可以感知终止信号
    const result = await f(newController.signal, ...args);
    // 操作完成后清空引用(可选,避免内存泄漏)
    if (currentController === newController) {
      currentController = null;
    }
    return result;
  } catch (err) {
    // 捕获终止信号的错误,避免控制台报错(如果需要)
    if (err.name === "AbortError") {
      console.log("旧操作已被终止");
    } else {
      throw err;
    }
  }
}

2. 简化await包裹的繁琐操作

可以封装一个工具函数,让它自动处理所有异步调用的终止逻辑,更稳妥的方式是封装带终止信号的异步工具,让函数内部直接使用:

// 封装一个带终止信号的等待函数
function waitWithAbort(ms, signal) {
  return new Promise((resolve, reject) => {
    if (signal.aborted) {
      return reject(signal.reason);
    }
    const timer = setTimeout(resolve, ms);
    // 监听终止信号,清理定时器并拒绝Promise
    signal.addEventListener("abort", () => {
      clearTimeout(timer);
      reject(signal.reason);
    }, { once: true });
  });
}

// 改造test3函数,直接接收信号
async function test3(signal) {
  console.log("[test3] A");
  await waitWithAbort(3000, signal);
  console.log("[test3] B");
  return 42;
}

// 使用runFunction调用
document.getElementById("buttonstart").addEventListener("click", async () => {
  await runFunction(test3);
});

对于第三方异步函数,如果它本身支持AbortSignal(比如fetch),直接传入信号即可;如果不支持,就用你之前写的makeMeAbortIfNeeded包裹,或者在封装的工具函数里统一处理。

关键注意事项

  • 无法强制终止同步代码:JavaScript是单线程的,AbortController只能通过拒绝Promise来终止异步等待,同步代码会执行完毕才能响应终止信号
  • 手动清理资源:在终止时,要手动清理占用的资源(比如WebCodec的Decoder实例),避免内存泄漏
  • 避免全局污染:尽量不要修改全局Promise,而是通过封装工具函数的方式让异步函数主动接收终止信号

内容的提问来源于stack exchange,提问作者tobiasBora

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 16:15:38