前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
首页程序员面试题库异步并发池实现
JavaScript异步与 Promise

手写一个限制最大并发数的异步任务调度器,怎么实现?

核心就两样东西:一个等待队列,一个正在运行的计数器。add 把任务放进队列并返回一个 Promise,只要运行数没到上限就从队列里取任务来跑;任务结束时计数减一,再去队列里取下一个。

前端进阶之旅 · 一题精讲更新于 2026.09.29
JavaScript#异步与 Promise#异步编程#并发编程
先看核心答案读代码示例
理解线索

调度器的三个部件

  1. 等待队列存还没开始的任务函数,以及它对应的 resolve 和 reject
  2. running 计数当前正在跑的任务数,小于上限才允许取新任务
  3. 补位任务在 finally 里计数减一,再调一次 run() 从队列取下一个

队列里必须放返回 Promise 的函数,而不是 Promise 本身。Promise 一创建任务就开始执行了,放进队列再排队已经晚了,并发限制就失效了。

核心回答

先记住这个答案

用一个等待队列和一个运行计数器就能实现。调用 add(task) 时,返回一个新的 Promise,把任务函数连同这个 Promise 的 resolve 和 reject 一起放进队列,然后尝试启动。启动的规则是:只要正在运行的数量小于上限、队列里还有任务,就取出一个执行并把计数加一。任务结束时,不管成功还是失败,都把计数减一,再尝试启动一次,空出来的位置就被下一个任务补上。

  • 等待队列存任务,计数器记正在运行的数量
  • add 返回 Promise,把 resolve 和任务一起存进队列
  • 任务结束(成功或失败)都要计数减一并补位
  • 队列里存函数,不要存已经创建的 Promise

面试最常考的写法和它的输出

这道题常见的题面是:最多同时跑 2 个任务,依次加入耗时 1000、500、300、400 毫秒的任务,问打印顺序。

队列加计数器的 SchedulerJavaScript
class Scheduler {
  constructor(limit) { this.limit = limit; this.running = 0; this.queue = [] }
  add(taskFn) {
    return new Promise((resolve, reject) => {
      this.queue.push({ taskFn, resolve, reject }) // 先排队,resolve 留到任务真正完成时再调
      this.run()
    })
  }
  run() {
    while (this.running < this.limit && this.queue.length) {
      const { taskFn, resolve, reject } = this.queue.shift()
      this.running++
      taskFn().then(resolve, reject).finally(() => { this.running--; this.run() }) // 结束就补位
    }
  }
}

const s = new Scheduler(2)
const wait = (ms) => () => new Promise((r) => setTimeout(r, ms))
const addTask = (ms, order) => s.add(wait(ms)).then(() => console.log(order))
addTask(1000, 1); addTask(500, 2); addTask(300, 3); addTask(400, 4)
查看输出与解释
2
3
1
4

推演一下时间线。一开始任务 1 和 2 同时开跑,3 和 4 在队列里等。第 500 毫秒任务 2 完成,打印 2,空出的位置给任务 3,它在 800 毫秒完成,打印 3,接着任务 4 开跑,要到 1200 毫秒才结束。任务 1 在 1000 毫秒完成,打印 1。最后打印 4。

为什么 add 要返回 Promise

调用方需要知道自己加进去的那个任务什么时候完成、结果是什么。但任务可能要在队列里等很久才开始,add 调用时根本拿不到结果。

所以 add 先返回一个「欠条」,也就是新建的 Promise,并把兑现欠条的 resolve 存进队列。等这个任务真的跑完,调度器再调用 resolve 把结果交出去。调用方只管 await s.add(task),不用关心任务排了多久的队。

这也是为什么队列里存的是 taskFn 这个函数。函数只是「待办事项」,调用它才会真正发请求。如果调用方传进来的是 request() 返回的 Promise,请求在传参那一刻就已经发出去了,调度器再怎么排队也限制不了并发。

这份实现还有哪些漏洞要补

面试时写出上面的版本基本合格,能主动补上下面两点会加分。

任务同步抛错
如果 taskFn 本身不是 async 函数,执行时直接 throw,那么 taskFn().then(...) 这一行根本走不到 .then。这时 running 已经加了一,却永远不会减,调度器会少一个位置。改成 Promise.resolve().then(taskFn),同步错误也会变成拒绝,统一走 finally。
任务一直不结束
某个请求挂住不返回,它占的位置就一直不释放。上限是 2 的话,挂住两个整个调度器就停了。实际项目里要给每个任务加超时,比如传一个 AbortSignal.timeout(ms) 给请求,超时就拒绝,位置自然被释放。
回答前,多想一步

容易答错的地方

只在成功时计数减一
把 running-- 写在 .then 里,任务一失败,计数就永远少减一次。失败几次之后,调度器就不再执行任何任务。计数必须放在 finally 里,或者成功和失败两条路都写。
用 Promise.race 在外面凑并发
有人先把所有任务都调用一遍拿到 Promise,再用 Promise.race 等空位。这时任务早就全部开始了,限制的只是「等待」而不是「执行」,和题目要求不符。
忘了在任务结束后再调一次 run()
只在 add 里启动,队列里剩下的任务要等下一次 add 才会被取出来。没有新的 add,它们就永远在排队。
试着用自己的话回答

面试官还会怎么问?

如果题目改成传入一组任务、按原顺序返回全部结果呢?

那就变成了「限制并发的 Promise.all」。在加入时记下每个任务的下标,完成后写到 results[index],全部完成后返回整个数组。结果顺序和完成顺序无关,只看下标。

怎么支持任务优先级?

把队列换成按优先级排序的结构。数量少时每次 push 后按优先级排一次序就够了,任务很多时用二叉堆,取出最高优先级是 O(log n)。

这个调度器和浏览器同域名的连接数限制有什么关系?

HTTP/1.1 下浏览器对同一个域名同时打开的连接数有限制,超出的请求会在浏览器内部排队。自己写调度器的好处是排队由代码控制,可以决定谁先谁后、能不能取消,还能把重要请求排在前面,而不是交给浏览器随意排。

从一道题,走向一组知识

把知识连起来

异步与 Promise

给 fetch 加重试时,怎么保证每次重试带的参数都和第一次一样?

调度器里跑的往往是请求,失败重试最好包在任务函数内部,调度器只管排队。

异步与 Promise

一个请求既要能被用户取消又要有超时,AbortSignal.any 怎么用?

给调度器加「全部取消」时,可以把外部取消信号和单个任务的超时用 AbortSignal.any 合在一起。

参考资料

  • JavaScript:异步与 Promise参考资料

示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。

本题目录
  1. 先记住这个答案
  2. 面试最常考的写法和它的输出
  3. 为什么 add 要返回 Promise
  4. 这份实现还有哪些漏洞要补
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

先看核心答案,再读代码。最后展开追问,检查自己有没有遗漏边界。

试着回答追问
浏览全部面试题理解原理,也关注真实的使用场景。回到顶部 ↑