异步任务并发控制

JavaScript 异步任务并发控制

🤔 问题背景

常见面试题 批量并发任务

📋 需求分析

一个完整的并发控制方案需要满足以下要求:

  • 并发限制:同时执行的任务数量不能超过指定上限
  • 任务队列:待执行的任务需要有序排队
  • 结果保序:无论任务何时完成,最终结果要按原始顺序返回
  • 错误处理:任何一个任务失败时,能够优雅地处理错误
  • 动态调度:任务完成后自动开始下一个待执行任务

💡 核心实现

让我们来看一个优雅的实现方案:

javascript 复制代码
const runTask = async (tasks, maxTaskNum) => {
  // 参数校验和初始化
  const total = Array.isArray(tasks) ? tasks.length : 0;
  if (total === 0) return [];

  const limit = Math.max(1, Math.min(maxTaskNum, total));
  const result = new Array(total);

  // 使用Promise.withResolvers()创建可控制的Promise
  const { promise, resolve, reject } = Promise.withResolvers();
  let nextIndex = 0; // 下一个要执行的任务索引
  let finished = 0; // 已完成的任务计数

  const runNext = () => {
    const i = nextIndex++;
    if (i >= total) return; // 没有更多任务

    Promise.resolve()
      .then(() => tasks[i]())
      .then((res) => {
        result[i] = res; // 按索引存储,保证顺序
      })
      .catch((err) => {
        reject(err); // 任何一个任务失败,整体失败
      })
      .finally(() => {
        finished++;
        if (finished === total) {
          resolve(result); // 所有任务完成
        } else {
          runNext(); // 继续执行下一个任务
        }
      });
  };

  // 启动初始的并发任务
  for (let i = 0; i < limit; i++) {
    runNext();
  }

  await promise;
  return result;
};

🔍 代码详解

1. 参数处理与初始化

javascript 复制代码
const total = Array.isArray(tasks) ? tasks.length : 0;
if (total === 0) return [];

const limit = Math.max(1, Math.min(maxTaskNum, total));
const result = new Array(total);

这部分代码确保了参数的合法性:

  • 验证任务数组的有效性
  • 计算实际并发数(不能超过总任务数,至少为 1)
  • 预先创建结果数组,确保索引对应关系

2. Promise 控制器

javascript 复制代码
const { promise, resolve, reject } = Promise.withResolvers();

Promise.withResolvers()是 ES2024 的新特性,它返回一个包含 promise 及其控制函数的对象,让我们可以在外部控制 Promise 的状态。

3. 任务调度核心

javascript 复制代码
const runNext = () => {
  const i = nextIndex++;
  if (i >= total) return;

  Promise.resolve()
    .then(() => tasks[i]())
    .then((res) => (result[i] = res))
    .catch((err) => reject(err))
    .finally(() => {
      finished++;
      if (finished === total) {
        resolve(result);
      } else {
        runNext();
      }
    });
};

这是整个调度器的核心逻辑:

  • nextIndex++:原子性地获取下一个任务索引
  • Promise.resolve().then():确保任务异步执行
  • result[i] = res:按原始索引存储结果
  • finally块:无论成功失败都要更新计数和调度

🎯 执行流程演示

让我们通过一个具体例子来理解执行流程:

javascript 复制代码
runTask(
  [
    () => new Promise((resolve) => setTimeout(() => resolve(1), 6000)), // 6秒
    () => new Promise((resolve) => setTimeout(() => resolve(2), 1000)), // 1秒
    () => new Promise((resolve) => setTimeout(() => resolve(3), 100)), // 0.1秒
    () => new Promise((resolve) => setTimeout(() => resolve(4), 2000)), // 2秒
    () => new Promise((resolve) => setTimeout(() => resolve(5), 100)), // 0.1秒
  ],
  2
).then((res) => {
  console.log(res); // [1, 2, 3, 4, 5]
});

执行时间线(并发数=2):

makefile 复制代码
0ms:     启动任务0(6s) 和 任务1(1s)      [执行中: 0,1]
1000ms:  任务1完成,启动任务2(0.1s)      [执行中: 0,2]
1100ms:  任务2完成,启动任务3(2s)        [执行中: 0,3]
3100ms:  任务3完成,启动任务4(0.1s)      [执行中: 0,4]
3200ms:  任务4完成,等待任务0            [执行中: 0]
6000ms:  任务0完成,所有任务结束         [完成]

总耗时: 6秒(相比串行执行的9.3秒,节省了3.3秒)

🚀 优化和扩展

1. 添加进度回调

javascript 复制代码
const runTaskWithProgress = async (tasks, maxTaskNum, onProgress) => {
    // ... 原有代码

    .finally(() => {
        finished++;
        onProgress && onProgress({
            finished,
            total,
            percent: (finished / total * 100).toFixed(2)
        });
        // ... 后续逻辑
    });
};

2. 支持任务优先级

javascript 复制代码
const runTaskWithPriority = async (tasks, maxTaskNum) => {
  // 按优先级排序任务
  const sortedTasks = tasks
    .map((task, index) => ({ task, index, priority: task.priority || 0 }))
    .sort((a, b) => b.priority - a.priority);

  // ... 使用排序后的任务执行
};

3. 失败重试机制

javascript 复制代码
const executeWithRetry = async (task, retries = 3) => {
  for (let i = 0; i < retries; i++) {
    try {
      return await task();
    } catch (error) {
      if (i === retries - 1) throw error;
      await new Promise((resolve) => setTimeout(resolve, 1000 * i));
    }
  }
};

⚡ 性能考量

  1. 内存使用:预先创建结果数组会占用内存,对于大量任务需要考虑分批处理
  2. 错误处理:当前实现遇到错误会立即终止,可以考虑支持部分失败
  3. 取消机制:长时间运行的任务可能需要支持取消操作

🎉 总结

异步任务并发控制是前端开发中的重要技能,它能够:

  • 提升性能:合理利用并发,减少总执行时间
  • 保护资源:避免过度并发造成的资源浪费
  • 增强体验:提供可控的执行进度和错误处理

通过理解其核心原理和实现细节,我们可以根据具体场景进行定制和优化,构建出更加 robust 和高效的异步任务处理方案。

这种模式在现代前端框架中也有广泛应用,比如 Vue 的异步组件加载、React 的 Suspense 机制等,都体现了类似的并发控制思想。掌握这种技术,将让你在处理复杂异步场景时更加得心应手。


如果这篇文章对你有帮助,欢迎点赞 👍、收藏 ⭐ 和分享 📤!

关注我,获取更多前端技术干货和面试题解析 🚀

相关推荐
崔庆才丨静觅20 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606121 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了21 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅21 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅21 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅1 天前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment1 天前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅1 天前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊1 天前
jwt介绍
前端
爱敲代码的小鱼1 天前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax