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)后能正常运行,但还有两个问题:
- 不知道如何把AbortController重置为未终止状态
- 每次都要手动包裹所有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
相关产品推荐
相关产品推荐

