🌍🌍🌍字节一面场景题:异步任务调度器

题目要求

实现一个 JavaScript 异步任务调度器 Scheduler,要求同时运行的任务数量不超过 n

示例

例如目前有4 个任务,所需执行时间分别为,1000ms、500ms、300ms、400ms。

那么当 n === 2 的时候,在该调度器中的执行完成顺序应该为 2、3、1、4。

0ms: 任务1、2开始执行 500ms: 任务2完成,任务3开始 800ms: 任务3完成,任务4开始 1000ms: 任务1完成 1200ms: 任务4完成

设计思路

模拟异步任务

为了测试调度器,我们需要模拟异步任务(比如接口请求)。

javascript 复制代码
/**
 * 创建一个延迟执行的任务
 * @param {Function} task - 要执行的任务函数
 * @param {number} delay - 延迟时间(毫秒)
 * @returns {Function} 返回一个返回 Promise 的函数
 */
const createTask = (task, delay) => {
  return () => {
    return new Promise((resolve) => {
      setTimeout(() => {
        task();
        resolve();
      }, delay);
    });
  };
};

createTask 函数接收两个参数:任务函数和延迟时间,返回一个返回 Promise 的函数。

为什么要返回函数而不是直接返回 Promise?

核心原因是控制执行时机:

  1. 直接返回 Promise:Promise 一旦创建就会立即执行,setTimeout 立刻开始计时,这样任务就不受调度器控制了。
  2. 返回函数:函数不会立即执行,只有调度器在合适的时机调用 task() 时,Promise 才会被创建,任务才真正开始执行。

简单来说,就是把"执行权"交给调度器。调度器通过 await task() 来决定何时执行任务,这样才能实现并发控制。这就像给调度器一个"开关",而不是一个已经启动的任务。

调度器设计

为了支持创建多个独立的调度器实例,同时保护内部状态不被外部直接修改,我们选择实现一个 Scheduler 类。

构造函数

根据题目要求"同时运行的任务数量不超过 n 个",我们需要在构造函数中定义三个关键变量:

  • maxConcurrency:最大并发数,通过构造函数传入,作为并发控制的上限
  • runningCount:当前正在执行的任务数量,用于判断是否达到并发上限
  • queue:等待队列,用于存储超出并发限制后需要等待的任务

注意:这里 queue 存储的不是任务本身,而是 Promise 的 resolve 函数。这是设计的关键点------我们不需要管理任务队列,只需要一个"唤醒机制"。当有任务完成时,调用队列中的 resolve() 就能让等待的任务继续执行。

核心功能

功能 1:判断是否需要等待?通过对比maxConcurrencyrunningCount 来判断

功能 2:如何让任务"挂起"等待?显然需要创建一个 promise ,将其resolve 函数放到queue 中等待队列

功能 3:执行任务,执行前后更新runningCount

功能 4:任务完成后,通过shift 调用queue中的resolve唤醒下一个任务

代码实现

可复制到控制台执行:

javascript 复制代码
/**
 * 任务调度器类
 * 用于控制并发任务的执行数量,防止同时执行过多任务
 */
class Scheduler {
  constructor(maxConcurrency) {
    this.queue = []; // 等待队列,存储的是 Promise 的 resolve 函数
    this.runningCount = 0; // 当前正在执行的任务数量
    this.maxConcurrency = maxConcurrency; // 最大并发数
  }

  /**
   * 添加任务到调度器
   * @param {Function} task - 要执行的异步任务
   * @returns {Promise} 返回任务执行结果
   */
  async addTask(task) {
    // 如果当前运行的任务数已达到最大并发数,则需要等待
    if (this.runningCount >= this.maxConcurrency) {
      // 核心设计:不存储任务本身,而是创建一个 Promise 让当前任务在此处等待
      // 将这个 Promise 的 resolve 函数存入队列,等有空闲时再调用它来恢复执行
      await new Promise((resolve) => {
        this.queue.push(resolve);
      });
    }

    // 开始执行任务
    this.runningCount++;
    let res = await task(); // 等待任务执行完成
    this.runningCount--; // 任务完成,运行数减 1

    // 如果有任务在等待,唤醒队列中的下一个任务
    if (this.queue.length) {
      const resolve = this.queue.shift(); // 取出等待队列的第一个 resolve
      resolve(); // 调用 resolve,让对应的任务从 await 处继续执行
    }

    return res;
  }
}

/**
 * 创建一个mock的异步任务
 * @param {Function} task - 要执行的任务函数
 * @param {number} delay - 延迟时间(毫秒)
 * @returns {Function} 返回一个返回 Promise 的函数
 */
const createTask = (task, delay) => {
  return () => {
    return new Promise((resolve) => {
      setTimeout(() => {
        task();
        resolve();
      }, delay);
    });
  };
};

// 创建一个最大并发数为 2 的调度器实例
const scheduler = new Scheduler(2);

scheduler.addTask(
  createTask(() => {
    console.log("任务 1");
  }, 1000),
);

scheduler.addTask(
  createTask(() => {
    console.log("任务 2");
  }, 500),
);

scheduler.addTask(
  createTask(() => {
    console.log("任务 3");
  }, 300),
);

scheduler.addTask(
  createTask(() => {
    console.log("任务 4");
  }, 400),
);

思考

  1. 这个问题在实际开发中有哪些应用场景?

这个模式在实际应用中非常常见,比如:

  • 控制 HTTP 请求并发数
  • 限制文件读写操作的并发
  • 批量数据处理时的流量控制
  1. 都说 JavaScript 是单线程的,怎么会有并发呢?

JavaScript 引擎是单线程执行的,但浏览器本身是多进程多线程模型。

  • JS 引擎线程:单线程,负责执行 JavaScript 代码

  • 定时器触发线程:负责处理 setTimeout、setInterval

  • HTTP 异步请求线程:负责处理网络请求

  • 事件触发线程:负责管理事件队列

  • GUI 渲染线程:负责页面渲染

当我们发起异步操作(如 fetch、setTimeout)时,实际上是把任务交给了浏览器的其他线程处理。这些线程可以真正并行工作,完成后通过事件循环(Event Loop)把回调放入任务队列,再由 JS 引擎线程执行。

  1. 如果要实现任务优先级,该如何改造?

可以把 queue 从数组改为优先队列(最小堆),每个等待的任务带上优先级。任务完成时,从堆中取出优先级最高的任务唤醒。

javascript 复制代码
// 存储格式:{ resolve, priority }
this.queue.push({ resolve, priority: task.priority })
// 唤醒时按优先级排序
const { resolve } = this.queue.sort((a, b) => a.priority - b.priority).shift()

只需要在任务工厂里加上优先级:

typescript 复制代码
// 改造 createTask,支持优先级
const createTask = (task, delay, priority = 0) => {
  const taskFn = () => {
    return new Promise((resolve) => {
      setTimeout(() => {
        task()
        resolve()
      }, delay)
    })
  }
  
  // ✅ Function 是 Object 的子类型,可以添加属性
  taskFn.priority = priority
  return taskFn
}
相关推荐
烛阴3 小时前
Lua字符串的利刃:模式匹配的艺术与实践
前端·lua
艾莉丝努力练剑3 小时前
【C++:继承和多态】多态加餐:面试常考——多态的常见问题11问
开发语言·c++·人工智能·面试·继承·c++进阶
奇舞精选3 小时前
一文了解 Server-Sent Events (SSE):构建高效的服务器推送应用
前端
Yeats_Liao3 小时前
Go Web 编程快速入门 11 - WebSocket实时通信:实时消息推送和双向通信
前端·后端·websocket·golang
纯爱掌门人3 小时前
鸿蒙状态管理V2实战:从零构建MVVM架构的应用
前端·harmonyos
丘耳3 小时前
vis-network 知识点笔记
前端·javascript
有点笨的蛋3 小时前
重新理解 Flexbox:让布局回归“弹性”的本质
前端·css
小着3 小时前
微信小程序组件中二维码生成问题解决方案
前端·微信小程序
潜心编码3 小时前
基于Django的医疗电子仪器系统
前端·数据库·1024程序员节