手写异步任务调度器(Async Scheduler)
面试高频手写题。考察点:并发控制、Promise 链、队列调度思想。
🎯 题目要求
实现一个 Scheduler 类,限制同时运行的异步任务最多为 N 个。超出的任务排队等候,前面的任务完成后自动拉取下一个执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| const scheduler = new Scheduler(2);
const timeout = (ms) => new Promise(resolve => setTimeout(resolve, ms));
async function addTask(delay, name) { await scheduler.add(() => timeout(delay)); console.log(name); }
addTask(1000, 'A'); addTask(500, 'B'); addTask(300, 'C'); addTask(400, 'D');
|
💡 核心思路
1 2 3 4 5 6
| 时间轴: 0ms → A(1000ms)、B(500ms) 同时开始(占满 2 个槽位) 500ms → B 完成,输出 B;立刻拉取 C(300ms) 开始 800ms → C 完成,输出 C;D 进入等待(A 还没完) 1000ms → A 完成,输出 A;立刻拉取 D(400ms) 开始 1400ms → D 完成,输出 D
|
关键:不是「等所有任务完成再批量开下一批」,而是「一个坑位空出来就立刻填」。
✍️ 实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| class Scheduler { constructor(max) { this.max = max; this.running = 0; this.queue = []; }
add(promiseCreator) { return new Promise((resolve, reject) => { this.queue.push({ promiseCreator, resolve, reject }); this._run(); }); }
_run() { while (this.running < this.max && this.queue.length > 0) { const { promiseCreator, resolve, reject } = this.queue.shift(); this.running++;
promiseCreator() .then(resolve, reject) .finally(() => { this.running--; this._run(); }); } } }
|
为什么传的是「函数」而不是「Promise」?
1 2 3 4 5
| scheduler.add(fetch('/api/data'));
scheduler.add(() => fetch('/api/data'));
|
🔍 逐行追踪(以上面的例子为例)
| 时间 |
事件 |
running |
queue |
| 0ms |
添加 A、B → 直接执行 |
2 |
[] |
| 0ms |
添加 C → 已满,入队 |
2 |
[C] |
| 0ms |
添加 D → 已满,入队 |
2 |
[C, D] |
| 500ms |
B 完成,running-- → _run() 拉取 C |
2 |
[D] |
| 800ms |
C 完成,running-- → _run() 尝试拉取 D,但 A 还在 |
2 |
[D] → 拉取 D |
| 1000ms |
A 完成,running-- → _run() 队列空 |
1 |
[] |
| 1400ms |
D 完成 |
0 |
[] |
🚀 进阶变体
变体 1:支持任务优先级
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class PriorityScheduler { constructor(max) { this.max = max; this.running = 0; this.queue = []; }
add(promiseCreator, priority = 0) { return new Promise((resolve, reject) => { this.queue.push({ promiseCreator, resolve, reject, priority }); this.queue.sort((a, b) => b.priority - a.priority); this._run(); }); }
_run() { while (this.running < this.max && this.queue.length > 0) { const { promiseCreator, resolve, reject } = this.queue.shift(); this.running++; promiseCreator() .then(resolve, reject) .finally(() => { this.running--; this._run(); }); } } }
|
变体 2:支持取消(AbortSignal)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| class Scheduler { constructor(max) { this.max = max; this.running = 0; this.queue = []; }
add(promiseCreator, signal) { return new Promise((resolve, reject) => { if (signal?.aborted) return reject(new DOMException('Aborted', 'AbortError'));
const task = { promiseCreator, resolve, reject }; this.queue.push(task);
signal?.addEventListener('abort', () => { const idx = this.queue.indexOf(task); if (idx !== -1) { this.queue.splice(idx, 1); reject(new DOMException('Aborted', 'AbortError')); } }, { once: true });
this._run(); }); }
_run() { while (this.running < this.max && this.queue.length > 0) { const { promiseCreator, resolve, reject } = this.queue.shift(); this.running++; promiseCreator() .then(resolve, reject) .finally(() => { this.running--; this._run(); }); } } }
|
变体 3:用 async/await 实现(面试偶尔考)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class Scheduler { constructor(max) { this.max = max; this.slots = []; }
async add(promiseCreator) { if (this.slots.length >= this.max) { await Promise.race(this.slots); }
const p = promiseCreator().finally(() => { this.slots.splice(this.slots.indexOf(p), 1); }); this.slots.push(p);
return p; } }
|
⚠️ 注意:Promise.race 变体在边界情况下有微妙的竞态问题(多个任务同时完成时),生产环境优先用队列版本。
🧠 面试延伸问题
Q:为什么不直接用 Promise.all?
A:Promise.all 是「全部同时发出,全部完成才返回」,没有并发限制。你有 100 个请求会全部同时打出去。
Q:这个模式在实际项目里用在哪?
A:图片/文件批量上传(限制同时上传数)、爬虫(限速防封 IP)、API 限流(遵守第三方 rate limit)。
Q:_run 里为什么用 while 而不是 if?
A:add 时可能槽位不止空出一个(比如批量 resolve)—— 不对,其实每次 finally 只完成一个,while 保证一次调用能把所有空槽填满(比如刚初始化时连续 add 多个任务)。
📌 一句话总结
维护一个「运行计数器」和「等待队列」。每次有任务完成就 -- 然后从队列头取下一个,只要计数器没超过上限就一直取。