前端并发控制管理

为了实现异步任务的并发控制,确保在最多 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 函数开始时启动了第一批任务,确保并发池被正确初始化。
相关推荐
Ticnix6 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人6 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl6 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅6 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人6 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼7 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端
布列瑟农的星空7 小时前
前端都能看懂的Rust入门教程(三)——控制流语句
前端·后端·rust
Mr Xu_7 小时前
Vue 3 中计算属性的最佳实践:提升可读性、可维护性与性能
前端·javascript
jerrywus7 小时前
我写了个 Claude Code Skill,再也不用手动切图传 COS 了
前端·agent·claude
玖月晴空7 小时前
探索关于Spec 和Skills 的一些实战运用-Kiro篇
前端·aigc·代码规范