返回
创建于
状态
公开
背错题的尴尬
在 JavaScript 中实现并发控制,通常有两种主流思路:一种是基于**信号量(计数器)的即时执行模式,另一种是基于任务队列(Queue)**的调度模式。 由于 JS 是单线程事件循环机制,我们通常利用 Promise 和 async/await 来模拟并发锁。
方案一:基于队列的通用调度器 (推荐)
这种方案最符合“类”的设计思想。你可以持续往 add 方法里丢任务,它会自动根据当前的执行情况决定是立即执行还是排队。
1class Scheduler {
2 constructor(max) {
3 this.max = max; // 最大并发数
4 this.count = 0; // 当前正在执行的任务数
5 this.queue = []; // 等待执行的任务队列
6 }
7
8 /**
9 * @param {() => Promise<any>} task 一个返回 Promise 的函数
10 */
11 add(task) {
12 return new Promise((resolve, reject) => {
13 // 将任务逻辑包装后放入队列
14 this.queue.push(async () => {
15 try {
16 const result = await task();
17 resolve(result);
18 } catch (err) {
19 reject(err);
20 }
21 });
22
23 this.run();
24 });
25 }
26
27 run() {
28 // 如果当前执行数未达上限,且队列中有任务,就出队执行
29 while (this.count < this.max && this.queue.length > 0) {
30 const task = this.queue.shift();
31 this.count++;
32
33 task().finally(() => {
34 this.count--;
35 this.run(); // 任务完成后,尝试触发下一个任务
36 });
37 }
38 }
39}
40
41// --- 测试代码 ---
42const timeout = (time) => new Promise(resolve => setTimeout(resolve, time));
43const scheduler = new Scheduler(2); // 限制并发为 2
44
45const addTask = (time, order) => {
46 scheduler.add(() => timeout(time))
47 .then(() => console.log(`任务 ${order} 完成`));
48};
49
50addTask(1000, '1');
51addTask(500, '2');
52addTask(300, '3');
53addTask(400, '4');
54
55// 预期输出:
56// 2 完成 (500ms)
57// 3 完成 (500+300=800ms)
58// 1 完成 (1000ms)
59// 4 完成 (800+400=1200ms)方案二:一次性批处理控制 (更简洁)
如果你有一组已知的任务数组,想要通过一个函数来控制它们的执行频率,可以使用这种递归模式。
1/**
2 * @param {Array<() => Promise<any>>} tasks 任务函数数组
3 * @param {number} limit 限制数
4 */
5async function asyncPool(tasks, limit) {
6 const results = [];
7 const executing = new Set(); // 正在执行的任务
8
9 for (const task of tasks) {
10 const p = Promise.resolve().then(() => task());
11 results.push(p);
12 executing.add(p);
13
14 // 任务执行完后从“执行中”集合移除
15 const clean = () => executing.delete(p);
16 p.then(clean).catch(clean);
17
18 // 如果达到并发上限,就用 Promise.race 等待其中一个完成
19 if (executing.size >= limit) {
20 await Promise.race(executing);
21 }
22 }
23
24 return Promise.all(results);
25}核心面试考点
- 闭包与 Promise 状态劫持:在方案一中,我们返回了一个新的 Promise 并在队列中保存了它的 resolve/reject,这是为了让调用者能像使用正常 await 一样获取结果。
- 递归触发:finally 块中的 this.run() 是关键,它形成了任务流的闭环,确保了“空出一个位子,补充一个任务”。
- 错误处理:确保 finally 或 catch 能正确减少计数器,否则一旦某个任务报错,并发位会被永久占用,导致程序“死锁”。