前端并发控制管理

为了实现异步任务的并发控制,确保在最多 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 函数开始时启动了第一批任务,确保并发池被正确初始化。
相关推荐
拾光拾趣录2 小时前
括号生成算法
前端·算法
拾光拾趣录3 小时前
requestIdleCallback:让你的网页如丝般顺滑
前端·性能优化
前端 贾公子3 小时前
vue-cli 模式下安装 uni-ui
前端·javascript·windows
拾光拾趣录3 小时前
链表合并:双指针与递归
前端·javascript·算法
@大迁世界3 小时前
前端:优秀架构的坟墓
前端·架构
期待のcode3 小时前
图片上传实现
java·前端·javascript·数据库·servlet·交互
hbrown4 小时前
Flask+LayUI开发手记(十一):选项集合的数据库扩展类
前端·数据库·python·layui
猫头虎4 小时前
什么是 npm、Yarn、pnpm? 有什么区别? 分别适应什么场景?
前端·python·scrapy·arcgis·npm·beautifulsoup·pip
迷曳4 小时前
27、鸿蒙Harmony Next开发:ArkTS并发(Promise和async/await和多线程并发TaskPool和Worker的使用)
前端·华为·多线程·harmonyos
安心不心安5 小时前
React hooks——useReducer
前端·javascript·react.js