为了实现异步任务的并发控制,确保在最多 max
个任务同时进行的情况下,一个任务完成后立即有另一个任务接上,需求:
思路:
- 任务计数器 : 维护当前正在运行的任务数量 (
runningTasksCount
)、已完成的任务数量 (completedTasksCount
) 和下一个待启动任务的索引 (nextTaskIndex
)。 - 结果收集 : 使用一个与任务总数相同大小的数组 (
results
) 来按原始顺序存储每个任务的结果。 - 递归启动 : 创建一个
launchTask
函数。当有空闲的并发槽位 (runningTasksCount < maxConcurrency
) 且有待启动的任务 (nextTaskIndex < totalTasks
) 时,就启动一个新任务。 - 任务完成回调 : 每个任务完成后,减少
runningTasksCount
,增加completedTasksCount
。 - 填补空位 : 任务完成后,立即尝试调用
launchTask
来填补因任务完成而释放的并发槽位。 - 最终回调 : 当所有任务都完成 (
completedTasksCount === totalTasks
) 时,调用最终的回调函数finalCallback
。 - 初始启动 : 在
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"]
核心点:
- 并发槽位管理 :
runningTasksCount
确保了同时运行的任务数量不会超过maxConcurrency
。 - 即时填补 : 当一个任务通过
.finally()
完成时,它会立即检查是否有新的任务可以启动 (if (nextTaskIndex < totalTasks) { launchTask(); }
),从而实现"一个异步任务完成后,另一个异步任务立即接上"的效果。 - 结果顺序 :
results
数组通过currentTaskIdx
确保了最终结果的顺序与原始任务的顺序一致,即使它们完成的顺序不同。 - 错误处理 :
catch
块允许你捕获并处理任务执行中的错误,并将错误信息记录在结果数组中。 - 初始启动 :
for
循环在control
函数开始时启动了第一批任务,确保并发池被正确初始化。