返回
创建于
状态
公开

背错题的尴尬

在 JavaScript 中实现并发控制,通常有两种主流思路:一种是基于**信号量(计数器)的即时执行模式,另一种是基于任务队列(Queue)**的调度模式。 由于 JS 是单线程事件循环机制,我们通常利用 Promise 和 async/await 来模拟并发锁。

方案一:基于队列的通用调度器 (推荐)

这种方案最符合“类”的设计思想。你可以持续往 add 方法里丢任务,它会自动根据当前的执行情况决定是立即执行还是排队。

javascript
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)

方案二:一次性批处理控制 (更简洁)

如果你有一组已知的任务数组,想要通过一个函数来控制它们的执行频率,可以使用这种递归模式。

javascript
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}

核心面试考点

  1. 闭包与 Promise 状态劫持:在方案一中,我们返回了一个新的 Promise 并在队列中保存了它的 resolve/reject,这是为了让调用者能像使用正常 await 一样获取结果。
  2. 递归触发:finally 块中的 this.run() 是关键,它形成了任务流的闭环,确保了“空出一个位子,补充一个任务”。
  3. 错误处理:确保 finally 或 catch 能正确减少计数器,否则一旦某个任务报错,并发位会被永久占用,导致程序“死锁”。