Dev KnowledgeV2.0
输入关键词开始搜索

    单线程 JavaScript 如何实现异步并发

    区分并发与并行,理解 await、事件循环和有界任务池各自负责什么。

    类型
    原理解释
    适合读者
    理解 async/await 语法、但希望弄清单线程为何能同时推进多个任务的开发者
    最近核验

    单线程限制的是执行 JavaScript 的时刻

    同一个 JavaScript 执行线程在同一时刻只执行一段代码,但多个任务可以同时处于未完成状态。请求发出后,网络传输由运行时和操作系统处理,远端服务独立计算;本地线程不需要一直占着 CPU 等待结果。

    并发表示多个任务的生命周期重叠、交错推进;并行表示不同执行资源在同一时刻实际工作。一个 JS 线程可以并发等待多个请求,但这不意味着它同时运行多段 JS。Node.js 内部、操作系统和远端服务也不只有这一个线程。来源:Node.js 事件循环与非阻塞 I/O。

    await 暂停当前函数,不阻塞整个线程

    调用请求函数会先执行其中的同步部分。它返回一个尚未完成的 Promise 后,await 暂停当前 async 函数,让其他就绪代码有机会执行;结果就绪后再恢复后续代码。

    下面以浏览器中的相对路径请求为例;在 Node.js 中调用 fetch 时需提供可解析的完整 URL。

    // 先发起三个请求,再等待全部结果。
    const requests = [fetch('/api/a'), fetch('/api/b'), fetch('/api/c')];
    const responses = await Promise.all(requests);

    这里启动请求的是三次 fetch() 调用;Promise.all() 负责组合和等待这些 Promise,不会创建线程。只声明三个 async 函数而不调用,也不会自动开始执行。

    若每个请求都主要等待远端约 10 秒,且服务端允许同时处理,三次串行等待约 30 秒;一起发出后,整体等待可以接近最慢的一次请求。实际耗时还受远端并发能力、限流和本地同步工作影响。

    异步等待后重新检查取消状态

    AbortSignal.throwIfAborted() 是同步检查:信号未取消时正常返回;已取消时原样抛出 signal.reason。它不会发出取消信号,也不会主动停止底层操作;触发取消由 AbortController.abort(reason) 负责。2026-10-14 的独立 Node.js 断言验证了取消前无异常、取消后抛出的对象与传入 reason 为同一对象。

    进入异步流程时通过检查,不代表每次 await 返回后仍然有效。等待期间,其他回调可能取消任务、释放资源或切换当前对象;底层 Promise 也可能在取消后仍正常完成。应在需要停止后续副作用或提交结果的边界重新检查,而不是只在函数入口检查一次。

    signal.throwIfAborted();
    await prepare();
    signal.throwIfAborted(); // 准备期间取消,就不再开始执行。
    const result = await execute();
    signal.throwIfAborted(); // 执行期间取消,就不提交迟到结果。
    currentResult = result;

    如果对象还支持独立 dispose,检查也需覆盖其释放状态。检查只负责阻止后续执行,不会撤销已发生的副作用,也不替代资源清理;释放连接或句柄应由对应取消处理与 catch/finally 负责。在同一 JS 执行线程中,没有 await 或会同步触发回调的调用隔开的普通检查与字段赋值之间,无须机械地再次检查。

    验证:2026-10-13 在 Node.js 用独立异步替身分别在创建、准备、就绪阶段触发 AbortController,三种取消均进入清理且不提交结果,未取消时正常提交。替身在取消后仍 resolve,用于验证“Promise 成功不代表任务仍有效”,未涉及真实网络或浏览器。

    并发上限由任务池的结构保证

    有界异步任务池可以启动固定数量的 async 循环。每条循环领取一个任务,等待它完成后才领取下一个;创建两条循环,就最多同时推进两个由该任务池管理的任务。

    如果多个循环共享任务游标,应在同一段同步代码中完成读取和递增,再 await 任务。普通 JS 回调不会在这段同步代码中途切入,因此不会重复领取同一个索引。若把读取和更新拆到 await 两侧,则需要重新考虑竞态。

    本地验证使用定时器模拟等待,四个任务分别等待 80、30、30、15 ms,并发上限为 2:A/B 先开始,B 约 32 ms 结束后开始 C,C 约 63 ms 结束后开始 D,最终约 80 ms 完成,峰值活动任务数为 2,且每个任务只执行一次。此验证说明等待可以重叠,并不是实际网络或模型吞吐测试。

    比较并发收益要测整批实际耗时

    并发会让不同任务的等待时间重叠。因此,整批实际经过时间(wall-clock duration)、单任务平均耗时和任务耗时之和回答的是不同问题:

    指标 回答的问题
    整批实际耗时 从整批开始到全部完成,用户等待多久?适合比较串行与并发。
    单任务平均耗时 一项任务通常多久完成?可用于观察拥塞或限流下的延迟变化。
    各任务耗时之和 累计占用了多少任务时间?重叠等待会被多次计入,不是 CPU 时间,也不是用户等待时间。

    计时应包住整批调度并等待所有任务结束,而不是对单项耗时求和。Node.js 的 performance.now() 提供以进程启动为原点的高精度毫秒时间戳,可用结束值减开始值测量区间。来源:Node.js Performance API。

    2026-10-09 的独立定时器实验中,4 个各等待 40 ms 的任务,串行整批约 164 ms,并发上限 2 时整批约 83 ms;后者各任务耗时之和仍约 165 ms。这里只验证等待重叠与统计口径,不代表真实服务能获得同样的加速。

    实际加速比应使用“同等任务串行的整批时间 ÷ 并发的整批时间”,并保持任务数量、输入和计时边界一致。明确是否包含初始化、收尾或上传;只有平均值的历史记录不能补算实际整批耗时。

    使用虚拟定时器测试时,也要替换被测代码实际调用的计时源。本地 Jest 验证中,推进虚拟 setTimeout 不会自动推进从 node:perf_hooks 导入的 performance.now();需要让它显式读取同一虚拟时钟,否则定时任务虽然完成了,测得的仍可能是真实执行测试代码的几毫秒。

    CPU 计算与共享状态仍需另行处理

    把耗时的同步循环放进 async 函数,或在其调用前加 await,不会把计算移到另一个线程;这段同步计算结束前,其他 JS 任务仍会被阻塞。CPU 密集工作需要考虑拆分执行、Worker 或子进程。来源:Node.js Worker threads 的适用范围。

    异步并发任务还会共享当前执行环境中的对象。是否为每个任务复制输入、隔离会话和处理过期响应,属于应用自身的状态管理;并发调度不自动提供这种隔离。进程、线程和 IPC 的区别见工程工具与系统速记。