JavaScript是单线程语言,异步任务的处理依赖事件循环机制,当我们需要同时处理多个异步任务时,直接全部发起执行很容易引发性能问题,因此需要设计合理的并发控制与异步任务调度算法来平衡执行效率和资源占用。

为什么需要异步任务并发控制
在实际开发中,我们经常会遇到批量处理异步任务的需求,比如需要同时上传100个文件、批量请求200个接口数据。如果直接循环发起所有异步任务,会有两个明显的问题:
- 同时发起的请求数量过多,可能被服务端限流,导致部分请求失败
- 大量异步任务同时执行会占用过多内存和CPU资源,影响页面其他功能的正常运行
并发控制的核心就是限制同一时间执行的异步任务数量,在保证执行效率的同时避免资源过度消耗。
基础异步任务调度思路
异步任务调度的核心逻辑是维护一个任务队列和一个正在执行的任务池,当任务池中的任务数量小于最大并发数时,就从任务队列中取出任务执行,当某个任务执行完成后,再从队列中取新任务补充到任务池中,直到所有任务都执行完成。
核心概念说明
- 任务队列:存放所有待执行的异步任务,通常是一个数组结构
- 最大并发数:同一时间允许同时执行的异步任务最大数量,由开发者根据场景设定
- 执行中任务池:存放当前正在执行的任务对应的Promise,用于判断当前并发数
并发控制调度算法实现
下面我们实现一个通用的异步任务并发控制函数,支持自定义最大并发数,返回所有任务的最终执行结果。
基础版实现代码
/**
* 异步任务并发控制函数
* @param {Array<Function>} tasks 任务数组,每个元素是一个返回Promise的函数
* @param {number} maxConcurrency 最大并发数
* @returns {Promise<Array>} 所有任务的结果数组,顺序和任务数组顺序一致
*/
async function asyncScheduler(tasks, maxConcurrency) {
// 参数校验
if (!Array.isArray(tasks) || tasks.length === 0) {
return [];
}
if (typeof maxConcurrency !== 'number' || maxConcurrency <= 0) {
throw new Error('最大并发数必须是大于0的数字');
}
const results = []; // 存放所有任务的结果
const executing = new Set(); // 正在执行的任务池,用Set存储Promise方便删除
let currentIndex = 0; // 当前待执行任务的索引
// 递归执行任务的函数
function runNextTask() {
// 如果所有任务都已经处理完成,直接返回
if (currentIndex >= tasks.length) {
return Promise.resolve();
}
const taskIndex = currentIndex;
const taskFn = tasks[currentIndex];
currentIndex++;
// 执行当前任务
const taskPromise = taskFn()
.then(result => {
results[taskIndex] = result; // 按原任务顺序存储结果
})
.catch(error => {
results[taskIndex] = error; // 错误也作为结果存储,后续可以统一处理
})
.finally(() => {
// 任务执行完成后从执行池中移除
executing.delete(taskPromise);
});
// 将当前任务的Promise加入执行池
executing.add(taskPromise);
// 如果当前执行池的任务数达到最大并发数,就等待任意一个任务完成后再继续
let nextPromise = Promise.resolve();
if (executing.size >= maxConcurrency) {
nextPromise = Promise.race(executing);
}
// 递归执行下一个任务
return nextPromise.then(() => runNextTask());
}
// 启动调度
await runNextTask();
// 等待所有任务执行完成
await Promise.all(executing);
return results;
}
代码逻辑解析
上述实现的核心逻辑如下:
- 首先校验入参,确保任务数组和最大并发数符合要求
- 使用
results数组按原任务顺序存储结果,executing集合存储正在执行的任务Promise,currentIndex记录当前待执行任务的索引 runNextTask函数负责执行下一个任务:如果还有未执行的任务,就取出执行,将Promise加入执行池;如果执行池数量达到最大并发数,就通过Promise.race等待任意一个任务完成后再继续取新任务- 任务完成后从执行池移除,同时按原索引存储结果,保证结果顺序和任务顺序一致
- 最终等待所有任务执行完成,返回结果数组
实际使用示例
下面模拟一个批量请求的场景,假设我们有10个接口请求任务,最大并发数设置为3,测试调度函数的效果。
// 模拟一个异步请求函数,随机返回成功或失败,耗时随机
function createRequestTask(id) {
return () => new Promise((resolve, reject) => {
const delay = Math.random() * 1000 + 500; // 随机500-1500ms耗时
setTimeout(() => {
if (Math.random() > 0.2) { // 80%概率成功
resolve(`任务${id}执行成功,耗时${delay.toFixed(0)}ms`);
} else { // 20%概率失败
reject(new Error(`任务${id}执行失败`));
}
}, delay);
});
}
// 生成10个任务
const tasks = [];
for (let i = 0; i < 10; i++) {
tasks.push(createRequestTask(i + 1));
}
// 调用调度函数,最大并发数设为3
asyncScheduler(tasks, 3).then(results => {
console.log('所有任务执行完成,结果如下:');
results.forEach((item, index) => {
if (item instanceof Error) {
console.log(`任务${index + 1}:失败,原因:${item.message}`);
} else {
console.log(`任务${index + 1}:成功,结果:${item}`);
}
});
}).catch(err => {
console.error('调度过程出现异常:', err);
});
执行上述代码会发现,同一时间最多只有3个任务在执行,不会出现所有任务同时发起的情况,符合并发控制的要求。
优化与扩展
基础的调度算法可以满足大部分场景的需求,实际使用中还可以根据需求做进一步的优化:
- 增加任务优先级支持,高优先级的任务优先进入执行队列
- 支持任务重试机制,当任务执行失败时自动重试指定次数
- 增加执行进度回调,实时返回当前已完成的任务数量和总任务数量
- 支持任务取消功能,当不需要继续执行时可以取消剩余未执行的任务
总结
JavaScript的异步任务并发控制核心是通过维护任务队列和执行池,限制同一时间的任务执行数量,避免资源过度消耗。本文介绍的调度算法实现简单、通用性强,可以直接应用到实际项目中。开发者可以根据具体的业务场景调整最大并发数,或者扩展更多自定义功能,让异步任务的处理更高效、更稳定。
JavaScript异步任务调度并发控制任务队列修改时间:2026-06-29 07:33:37