前端并发控制管理

为了实现异步任务的并发控制,确保在最多 max 个任务同时进行的情况下,一个任务完成后立即有另一个任务接上,需求:

思路:

  1. 任务计数器 : 维护当前正在运行的任务数量 (runningTasksCount)、已完成的任务数量 (completedTasksCount) 和下一个待启动任务的索引 (nextTaskIndex)。
  2. 结果收集 : 使用一个与任务总数相同大小的数组 (results) 来按原始顺序存储每个任务的结果。
  3. 递归启动 : 创建一个 launchTask 函数。当有空闲的并发槽位 (runningTasksCount < maxConcurrency) 且有待启动的任务 (nextTaskIndex < totalTasks) 时,就启动一个新任务。
  4. 任务完成回调 : 每个任务完成后,减少 runningTasksCount,增加 completedTasksCount
  5. 填补空位 : 任务完成后,立即尝试调用 launchTask 来填补因任务完成而释放的并发槽位。
  6. 最终回调 : 当所有任务都完成 (completedTasksCount === totalTasks) 时,调用最终的回调函数 finalCallback
  7. 初始启动 : 在 control 函数的开始,循环调用 launchTask maxConcurrency 次(或直到所有任务都启动),以填充初始的并发槽位。

代码

js 复制代码
/**
 * 并发控制函数
 * 确保最多 maxConcurrency 个异步任务同时进行,一个任务完成后立即接上新的任务。
 *
 * @param {Array<Function>} tasks - 异步任务函数数组,每个函数应返回一个 Promise。
 *                                  例如: [() => Promise.resolve(1), () => Promise.reject('error')]
 * @param {number} maxConcurrency - 最大并发数。
 * @param {Function} finalCallback - 所有任务完成后的回调函数,接收一个包含所有任务结果的数组。
 *                                   结果数组的顺序与 tasks 数组的顺序一致。
 */
const control = (tasks, maxConcurrency, finalCallback) => {
  const totalTasks = tasks.length;

  // 1. 处理无任务的情况
  if (totalTasks === 0) {
    return finalCallback([]);
  }

  // 2. 默认最大并发数:如果未指定或无效,则默认为所有任务同时执行
  if (!maxConcurrency || maxConcurrency <= 0) {
    maxConcurrency = totalTasks;
  }

  // 3. 初始化状态变量
  let completedTasksCount = 0; // 已完成的任务数量
  let runningTasksCount = 0;   // 当前正在运行的任务数量
  let nextTaskIndex = 0;       // 下一个待启动任务在 tasks 数组中的索引
  const results = new Array(totalTasks); // 存储任务结果的数组,保持原始顺序

  /**
   * 尝试启动一个新的任务
   * 这是一个递归函数,用于在任务完成时填补空闲的并发槽位。
   */
  const launchTask = () => {
    // 只有当还有任务待启动 并且 还有空闲的并发槽位时,才启动新任务
    if (nextTaskIndex < totalTasks && runningTasksCount < maxConcurrency) {
      const currentTaskIdx = nextTaskIndex; // 记录当前任务在原始数组中的索引
      const taskFn = tasks[nextTaskIndex];  // 获取任务函数
      
      nextTaskIndex++;                       // 移动到下一个待启动任务的索引
      runningTasksCount++;                   // 增加正在运行的任务计数

      // 执行任务函数(它应该返回一个 Promise)
      taskFn()
        .then(result => {
          results[currentTaskIdx] = result; // 任务成功,存储结果
        })
        .catch(error => {
          // 任务失败,存储错误信息(可以根据需要调整错误存储方式)
          results[currentTaskIdx] = { error: error, status: 'rejected' };
          console.error(`任务 ${currentTaskIdx} 失败:`, error);
        })
        .finally(() => {
          runningTasksCount--;   // 任务完成,减少正在运行的任务计数
          completedTasksCount++; // 增加已完成任务计数

          // 任务完成后,立即尝试启动下一个任务来填补空闲的槽位
          // 只要还有任务待启动,就继续尝试启动
          if (nextTaskIndex < totalTasks) {
            launchTask(); // 递归调用,尝试启动新任务
          }

          // 检查是否所有任务都已完成
          if (completedTasksCount === totalTasks) {
            finalCallback(results); // 所有任务完成,调用最终回调
          }
        });

      // 关键点:在启动一个任务后,立即尝试启动下一个任务
      // 这确保了在初始阶段,能够快速填满 maxConcurrency 个并发槽位
      // 而不需要等待第一个任务完成
      launchTask();
    }
  };

  // 4. 初始启动:启动首批任务,填满并发槽位
  // 循环 maxConcurrency 次(或直到所有任务都已启动,如果任务总数小于最大并发数)
  for (let i = 0; i < Math.min(maxConcurrency, totalTasks); i++) {
    launchTask();
  }
};

// --- 示例用法 ---

// 原始代码中的回调函数
const cb = (val) => {
  console.log("所有任务完成。结果:", val);
};

// 辅助函数:创建一个模拟异步任务的函数
// id: 任务标识符,delay: 模拟的延迟时间(毫秒)
const createTask = (id, delay, shouldFail = false) => {
  return () => {
    console.log(`任务 ${id} 开始执行...`);
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (shouldFail) {
          console.log(`任务 ${id} 失败!`);
          reject(`Error from task ${id}`);
        } else {
          console.log(`任务 ${id} 完成。`);
          resolve(`结果 ${id}`);
        }
      }, delay);
    });
  };
};

// 任务栈,包含多个异步任务函数
const stack = [
  createTask(0, 1000), // 1秒
  createTask(1, 500),  // 0.5秒
  createTask(2, 1200), // 1.2秒
  createTask(3, 300, true),  // 0.3秒,模拟失败
  createTask(4, 800),   // 0.8秒
  createTask(5, 700)    // 0.7秒
];

console.log("--- 启动并发控制 (最大并发数: 2) ---");
control(stack, 2, cb);

// 预期输出大致顺序(由于异步执行,具体时间点会有差异):
// 任务 0 开始执行...
// 任务 1 开始执行...
// (0.5秒后) 任务 1 完成。
// (立即) 任务 2 开始执行... (任务 1 完成后,任务 2 立即接上)
// (0.3秒后) 任务 3 开始执行... (任务 3 模拟失败)
// (0.3秒后,从任务 3 开始算) 任务 3 失败!
// (立即) 任务 4 开始执行... (任务 3 完成后,任务 4 立即接上)
// (0.2秒后,从任务 0 开始算) 任务 0 完成。
// (立即) 任务 5 开始执行... (任务 0 完成后,任务 5 立即接上)
// (0.7秒后,从任务 5 开始算) 任务 5 完成。
// (0.5秒后,从任务 4 开始算) 任务 4 完成。
// (0.5秒后,从任务 2 开始算) 任务 2 完成。
// 所有任务完成。结果: ["结果 0", "结果 1", "结果 2", { error: "Error from task 3", status: "rejected" }, "结果 4", "结果 5"]

核心点:

  1. 并发槽位管理 : runningTasksCount 确保了同时运行的任务数量不会超过 maxConcurrency
  2. 即时填补 : 当一个任务通过 .finally() 完成时,它会立即检查是否有新的任务可以启动 (if (nextTaskIndex < totalTasks) { launchTask(); }),从而实现"一个异步任务完成后,另一个异步任务立即接上"的效果。
  3. 结果顺序 : results 数组通过 currentTaskIdx 确保了最终结果的顺序与原始任务的顺序一致,即使它们完成的顺序不同。
  4. 错误处理 : catch 块允许你捕获并处理任务执行中的错误,并将错误信息记录在结果数组中。
  5. 初始启动 : for 循环在 control 函数开始时启动了第一批任务,确保并发池被正确初始化。
相关推荐
小妖666几秒前
react-router 怎么设置 basepath 设置网站基础路径
前端·react.js·前端框架
xvmingjiang6 分钟前
Element Plus 中 el-input 限制为数值输入的方法
前端·javascript·vue.js
XboxYan23 分钟前
借助CSS实现自适应屏幕边缘的tooltip
前端·css
极客小俊24 分钟前
iconfont 阿里巴巴免费矢量图标库超级好用!
前端
小杨 想拼31 分钟前
使用js完成抽奖项目 效果和内容自定义,可以模仿游戏抽奖页面
前端·游戏
yvvvy34 分钟前
🐙 Git 从入门到面试能吹的那些事
前端·trae
EmmaGuo20151 小时前
flutter3.7.12版本设置TextField的contextMenuBuilder的文字颜色
前端·flutter
pepedd8642 小时前
全面解析this-理解this指向的原理
前端·javascript·trae
渔夫正在掘金2 小时前
神奇魔法类:使用 createMagicClass 增强你的 JavaScript/Typescript 类
前端·javascript
雲墨款哥2 小时前
一个前端开发者的救赎之路-JS基础回顾(三)-Function函数
前端·javascript