问题背景
看到一篇《# 面试官:假如有几十个请求,如何去控制并发?》文章,加上最近也考虑过这样问题,于是尝试实现一下。
先简单描述下需求:假设有100个接口请求,如何控制当前执行的请求任务最大为5个,即并发执行有最大限制5个,等所有任务执行完成后,获取结果。
我们都知道Promise.all是并发执行了所有任务,那么上面需求就需要借助三方库或自己实现了。
大概思路如下:
- 有一个任务队列,先从队头取limit个任务,然后并发执行。
- 等某任务执行完成后,在从队头取一个任务执行。
- 重复步骤2,直到所有任务执行完成。
下面就一步一步来实现
一、先创建测试任务
function getTask(taskId) {
const timeout = Math.random() * 1000;
const run = () => new Promise((resolve, reject) => {
setTimeout(() => {
const isSuccess = parseInt(timeout) % 3 !== 0;
if (isSuccess) {
resolve({
success: true,
data: `success, ${taskId}`,
duration: timeout,
});
} else {
reject({
success: false,
data: `error, ${taskId}`,
duration: timeout,
});
}
console.log(`taskId: ${taskId}, done, duration: ${timeout}`);
}, timeout);
});
return {
taskId,
run,
}
}
function createTasks() {
return (new Array(20))
.fill(0)
.map((_, index) => getTask(index));
}
模拟创建了20个异步任务,其中任务Task的数据结构,包含两个成员taskId,run。
- taskId:表示任务id
- run函数:表示执行任务
- 还可以包含,比如:任务的结果,状态,耗时,开始执行时间等
二、实现并发控制逻辑
function parallel(tasks, limit = 5) {
if (limit <= 1) {
throw new Error('param limit error');
}
const firsts = tasks.slice(0, limit);
const rests = tasks.length > limit ? tasks.slice(limit) : [];
const results = [];
function onComplete() {
if (rests.length > 0) {
const task = rests.shift();
exec(task, onComplete);
return;
}
console.log('rests length === 0');
if (results.length === tasks.length) {
console.log('all done', /* results */);
}
}
function exec({ taskId, run }, callback) {
run().then((data) => {
results.push({
taskId,
data,
});
}).catch((data) => {
results.push({
taskId,
data,
});
}).finally(() => callback());
}
for (let task of firsts) {
exec(task, onComplete);
}
}
//执行
parallel(createTasks(), 5);
三、执行结果
taskId: 2, done, duration: 497.61017182430953
taskId: 4, done, duration: 584.145100059779
taskId: 1, done, duration: 613.4490547294704
taskId: 0, done, duration: 920.7273120938802
taskId: 3, done, duration: 920.6280649993439
taskId: 5, done, duration: 455.1027063423867
taskId: 8, done, duration: 482.1030881517208
taskId: 7, done, duration: 822.5554092945662
taskId: 6, done, duration: 909.467455409011
taskId: 9, done, duration: 627.1933377796586
taskId: 10, done, duration: 719.4576759668759
taskId: 12, done, duration: 344.4108433734165
taskId: 13, done, duration: 333.9694191524407
taskId: 11, done, duration: 528.6460453635787
taskId: 14, done, duration: 450.0735360611232
taskId: 18, done, duration: 131.33362456975405
rests length === 0
taskId: 17, done, duration: 460.4421209097864
rests length === 0
taskId: 15, done, duration: 675.46121186546
rests length === 0
taskId: 16, done, duration: 621.4313624801869
rests length === 0
taskId: 19, done, duration: 564.8101996316699
rests length === 0
all done
嗯,执行没问题。如果作为工具库,需要写单元测试,把各种异常边界check好,测试到位。
提问与思考
- 上面示例传入了一个任务数组,在任务执行的过程中,如何动态添加任务?
- 示例中,并发数量固定为limit,在任务执行过程中,如何动态修改最大并发数量?
- 请将上面示例,重构为面向对象编程。比如定义一个TaskExecutor类,包括动态添加任务方法AddTasks,动态修改最大并发数changeLimitCount,任务执行完成的事件,所有任务执行完成后事件等
- 小提问,rests length === 0 这行log会打印多少次,为什么?onComplete函数的第一行判断严谨吗,为什么?
以上,抛砖引玉,若有问题,欢迎指出,谢谢。欢迎大家讨论。
抛个问题:提出问题重要还是解决问题重要?哈哈,不是要解决提出问题的人哈~你怎么看?