异步任务并发控制
2025年8月18日 17:52
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. 参数处理与初始化
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 控制器
const { promise, resolve, reject } = Promise.withResolvers();
Promise.withResolvers()
是 ES2024 的新特性,它返回一个包含 promise 及其控制函数的对象,让我们可以在外部控制 Promise 的状态。
3. 任务调度核心
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
块:无论成功失败都要更新计数和调度
🎯 执行流程演示
让我们通过一个具体例子来理解执行流程:
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):
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. 添加进度回调
const runTaskWithProgress = async (tasks, maxTaskNum, onProgress) => {
// ... 原有代码
.finally(() => {
finished++;
onProgress && onProgress({
finished,
total,
percent: (finished / total * 100).toFixed(2)
});
// ... 后续逻辑
});
};
2. 支持任务优先级
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. 失败重试机制
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));
}
}
};
⚡ 性能考量
- 内存使用:预先创建结果数组会占用内存,对于大量任务需要考虑分批处理
- 错误处理:当前实现遇到错误会立即终止,可以考虑支持部分失败
- 取消机制:长时间运行的任务可能需要支持取消操作
🎉 总结
异步任务并发控制是前端开发中的重要技能,它能够:
- 提升性能:合理利用并发,减少总执行时间
- 保护资源:避免过度并发造成的资源浪费
- 增强体验:提供可控的执行进度和错误处理
通过理解其核心原理和实现细节,我们可以根据具体场景进行定制和优化,构建出更加 robust 和高效的异步任务处理方案。
这种模式在现代前端框架中也有广泛应用,比如 Vue 的异步组件加载、React 的 Suspense 机制等,都体现了类似的并发控制思想。掌握这种技术,将让你在处理复杂异步场景时更加得心应手。
如果这篇文章对你有帮助,欢迎点赞 👍、收藏 ⭐ 和分享 📤!
关注我,获取更多前端技术干货和面试题解析 🚀