上一篇《如何控制多个异步任务执行的最大并发数》,然后看到另一篇《js异步并发控制,限制请求数量的解惑》,其中的两种实现思路挺好的,值得学习。在此我对这两种实现做了一些代码优化,分享出来。
一、基于执行池+异步函数的实现
思路如下:
- 创建一个执行池。池内任务数量最大为limit个
- 当执行池中有任务完成后,获取一个任务,放入执行池中执行
- 当执行池中所有任务都执行完成,获取结果
上代码
function getTask(taskId) {
const timeout = Math.random() * 1000;
return () => new Promise(resolve => {
setTimeout(() => {
const isSuccess = parseInt(timeout) % 3 !== 0;
if (isSuccess) {
resolve({
taskId,
success: true,
data: `success, ${taskId}`,
duration: timeout,
});
} else {
resolve({
taskId,
success: false,
data: `error, ${taskId}`,
duration: timeout,
});
}
console.log(`taskId: ${taskId} done, isSuccess: ${isSuccess}, duration: ${timeout}`);
}, timeout);
});
}
function createTasks(size) {
return (new Array(size))
.fill(0)
.map((_, index) => getTask(index));
}
async function parallel2(tasks, limit = 5) {
if (limit <= 1) {
throw new Error('param limit error');
}
//任务数量小于等于最大并发数时,全部并发执行
if (tasks.length <= limit) {
return await Promise.all(tasks.map(task => task()));
}
//任务数量大于最大并发数时,需做并发数量控制
const excuting = new Set();//执行池
const results = [];
for (const task of tasks) {//从任务队列中获取任务
const p = task();
excuting.add(p);//添加到执行池
results.push(p);
if (excuting.size === limit) {//当excuting中数量等于limit时,需等待excuting中有某一个任务完成
const firstDone = await Promise.race(excuting);
excuting.delete(firstDone);//将某一个完成的任务,从excuting中移除
}
}
await Promise.all(excuting);//等待excuting中所有任务都完成
return results;
}
(async function () {
const tasks = createTasks(20);
const results = await parallel2(tasks, 5);
console.log('all done');
for await (const r of results) {
console.log(r);
}
})();
运行结果:
taskId: 0 done, isSuccess: true, duration: 365.3956221658572
taskId: 13 done, isSuccess: false, duration: 21.050019451194668
taskId: 2 done, isSuccess: true, duration: 424.4192465703498
taskId: 1 done, isSuccess: false, duration: 474.4346552481
taskId: 15 done, isSuccess: false, duration: 228.32069432547252
taskId: 16 done, isSuccess: true, duration: 251.3130958407921
taskId: 11 done, isSuccess: false, duration: 351.0530572210191
taskId: 3 done, isSuccess: true, duration: 823.5456217020765
taskId: 7 done, isSuccess: false, duration: 468.45783574679587
taskId: 4 done, isSuccess: true, duration: 866.4717103294523
taskId: 5 done, isSuccess: false, duration: 552.7296643290283
taskId: 12 done, isSuccess: false, duration: 582.5223955036955
taskId: 19 done, isSuccess: true, duration: 806.2824778187854
taskId: 10 done, isSuccess: false, duration: 819.9300388058152
taskId: 8 done, isSuccess: false, duration: 828.4914840344118
taskId: 17 done, isSuccess: true, duration: 857.3538874719449
taskId: 6 done, isSuccess: true, duration: 916.8293674255292
taskId: 14 done, isSuccess: true, duration: 920.363132996949
taskId: 18 done, isSuccess: false, duration: 960.5530614626679
taskId: 9 done, isSuccess: true, duration: 973.3625162321156
all done
{ taskId: 0,
success: true,
data: 'success, 0',
duration: 365.3956221658572 }
{ taskId: 1,
success: false,
data: 'error, 1',
duration: 474.4346552481 }
{ taskId: 2,
success: true,
data: 'success, 2',
duration: 424.4192465703498 }
{ taskId: 3,
success: true,
data: 'success, 3',
duration: 823.5456217020765 }
{ taskId: 4,
success: true,
data: 'success, 4',
duration: 866.4717103294523 }
{ taskId: 5,
success: false,
data: 'error, 5',
duration: 552.7296643290283 }
{ taskId: 6,
success: true,
data: 'success, 6',
duration: 916.8293674255292 }
{ taskId: 7,
success: false,
data: 'error, 7',
duration: 468.45783574679587 }
{ taskId: 8,
success: false,
data: 'error, 8',
duration: 828.4914840344118 }
{ taskId: 9,
success: true,
data: 'success, 9',
duration: 973.3625162321156 }
{ taskId: 10,
success: false,
data: 'error, 10',
duration: 819.9300388058152 }
{ taskId: 11,
success: false,
data: 'error, 11',
duration: 351.0530572210191 }
{ taskId: 12,
success: false,
data: 'error, 12',
duration: 582.5223955036955 }
{ taskId: 13,
success: false,
data: 'error, 13',
duration: 21.050019451194668 }
{ taskId: 14,
success: true,
data: 'success, 14',
duration: 920.363132996949 }
{ taskId: 15,
success: false,
data: 'error, 15',
duration: 228.32069432547252 }
{ taskId: 16,
success: true,
data: 'success, 16',
duration: 251.3130958407921 }
{ taskId: 17,
success: true,
data: 'success, 17',
duration: 857.3538874719449 }
{ taskId: 18,
success: false,
data: 'error, 18',
duration: 960.5530614626679 }
{ taskId: 19,
success: true,
data: 'success, 19',
duration: 806.2824778187854 }
说明:
- 模拟任务的数据结构有变化,任务的结果统一到Promise的fulfilled,方便后续使用Promise.race, Promise.all时不用在try catch。
- 任务池executing中,如果有任务完成,如何获得到通知?使用Promise.race。这里选择Set数据结构,方便移除已完成的任务。
- 异步函数,for。用同步的编码方式写异步,没有callback回调,提高可读性。
- 代码中已有注释,无需在详细解释。
二、通过Promise.all+“包裹Promise”对象+延迟任务上下文队列来实现
思路如下:
- 如果用Promise.all(allTasks),那么所有任务都会立即执行,怎么办?创建“包裹Promise对象” (先用这个词描述)列表。
- 如何将实际任务和包裹对象对应起来?创建一个queue,保存延迟任务上下文。
上代码
function getTask(taskId) {
const timeout = Math.random() * 1000;
return () => new Promise(resolve => {
setTimeout(() => {
const isSuccess = parseInt(timeout) % 3 !== 0;
if (isSuccess) {
resolve({
taskId,
success: true,
data: `success, ${taskId}`,
duration: timeout,
});
} else {
resolve({
taskId,
success: false,
data: `error, ${taskId}`,
duration: timeout,
});
}
console.log(`taskId: ${taskId} done, isSuccess: ${isSuccess}, duration: ${timeout}`);
}, timeout);
});
}
function createTasks(size) {
return (new Array(size))
.fill(0)
.map((_, index) => getTask(index));
}
// 队列
class Queue {
constructor() {
this._queue = [];
}
push(value) {
return this._queue.push(value);
}
shift() {
// TODO 优化出队操作 shift 操作的时间复杂度为 O(n)。使用 reverse + pop 的方式,引入双数组的设计,减低时间复杂度类O(1)
return this._queue.shift();
}
isEmpty() {
return this._queue.length === 0;
}
}
// 延迟任务
class DelayedTask {
constructor(resolve, fn, args) {
this.resolve = resolve;
this.fn = fn;
this.args = args;
}
}
// 任务管理
class TaskExecutor {
constructor(tasks, limit) {
if (limit <= 1) {
throw new Error('param limit error');
}
this.limit = limit;
this.queue = new Queue();
this.taskList = tasks.map(task => new Promise(resolve => {//创建所有任务的包裹Promise对象
this.queue.push(new DelayedTask(resolve, task, null));//将包裹Promise对象的resolve、原生任务fn,作为上下文入queue
}));
}
async _runTask() {
if (this.queue.isEmpty()) {
return;
}
const { resolve, fn } = this.queue.shift();
const p = fn();//任务的执行
resolve(p);//包裹Promise的resolve,当任务p完成后,包裹Promise对象状态变为完成
await p;//等任务p finish后,再执行一个任务
this._runTask();
}
start() {
//任务数量小于等于最大并发数时,全部并发执行
if (this.taskList.length <= this.limit) {
for (let i = 0; i < this.taskList.length; i++) {
this._runTask();
}
return;
}
//任务数量大于最大并发数时,执行limit个任务
for (let i = 0; i < this.limit; i++) {
this._runTask();
}
}
async getResult() {
return await Promise.all(this.taskList);
}
}
(async function () {
const tasks = createTasks(20);
const te = new TaskExecutor(tasks, 5);
te.start();
const results = await te.getResult();
console.log('all done');
for await (const r of results) {
console.log(r);
}
})();
执行结果:
taskId: 4 done, isSuccess: true, duration: 20.115767437477892
taskId: 0 done, isSuccess: false, duration: 105.848267415376
taskId: 1 done, isSuccess: false, duration: 522.7650385184304
taskId: 7 done, isSuccess: false, duration: 45.7575806591699
taskId: 3 done, isSuccess: true, duration: 820.2655914607603
taskId: 5 done, isSuccess: true, duration: 826.0219526308681
taskId: 2 done, isSuccess: false, duration: 864.9434351715912
taskId: 6 done, isSuccess: false, duration: 759.7088155858061
taskId: 8 done, isSuccess: false, duration: 534.1419844365685
taskId: 9 done, isSuccess: true, duration: 301.2653793162872
taskId: 10 done, isSuccess: false, duration: 306.419786407536
taskId: 12 done, isSuccess: true, duration: 386.97192279877714
taskId: 14 done, isSuccess: true, duration: 137.398348572382
taskId: 13 done, isSuccess: true, duration: 154.33079634396995
taskId: 15 done, isSuccess: false, duration: 126.50777983463124
taskId: 18 done, isSuccess: true, duration: 29.499091832386082
taskId: 19 done, isSuccess: false, duration: 114.079764638644
taskId: 16 done, isSuccess: true, duration: 337.5117443091564
taskId: 11 done, isSuccess: true, duration: 952.1976348952703
taskId: 17 done, isSuccess: true, duration: 575.4964472014608
all done
{ taskId: 0,
success: false,
data: 'error, 0',
duration: 105.848267415376 }
{ taskId: 1,
success: false,
data: 'error, 1',
duration: 522.7650385184304 }
{ taskId: 2,
success: false,
data: 'error, 2',
duration: 864.9434351715912 }
{ taskId: 3,
success: true,
data: 'success, 3',
duration: 820.2655914607603 }
{ taskId: 4,
success: true,
data: 'success, 4',
duration: 20.115767437477892 }
{ taskId: 5,
success: true,
data: 'success, 5',
duration: 826.0219526308681 }
{ taskId: 6,
success: false,
data: 'error, 6',
duration: 759.7088155858061 }
{ taskId: 7,
success: false,
data: 'error, 7',
duration: 45.7575806591699 }
{ taskId: 8,
success: false,
data: 'error, 8',
duration: 534.1419844365685 }
{ taskId: 9,
success: true,
data: 'success, 9',
duration: 301.2653793162872 }
{ taskId: 10,
success: false,
data: 'error, 10',
duration: 306.419786407536 }
{ taskId: 11,
success: true,
data: 'success, 11',
duration: 952.1976348952703 }
{ taskId: 12,
success: true,
data: 'success, 12',
duration: 386.97192279877714 }
{ taskId: 13,
success: true,
data: 'success, 13',
duration: 154.33079634396995 }
{ taskId: 14,
success: true,
data: 'success, 14',
duration: 137.398348572382 }
{ taskId: 15,
success: false,
data: 'error, 15',
duration: 126.50777983463124 }
{ taskId: 16,
success: true,
data: 'success, 16',
duration: 337.5117443091564 }
{ taskId: 17,
success: true,
data: 'success, 17',
duration: 575.4964472014608 }
{ taskId: 18,
success: true,
data: 'success, 18',
duration: 29.499091832386082 }
{ taskId: 19,
success: false,
data: 'error, 19',
duration: 114.079764638644 }
说明:
- 代码中已有注释。
关于对引用文中优化了哪些地方,大家可以自行对比,都在代码中。
js关于异步编程中,代码可读性排序:async > yield > Promise then > callback
最后来个个人感受总结:
- 上一篇是callback+直接想法的实现
- 本篇实现一,通过“同步顺序代码”的方式编写异步。后面准备用另外的题目写一篇。
- 本篇实现二,将Promise的resolve传递。此技巧可以用来解决一些时序控制问题等,后续准备另写一篇文章。
欢迎大家拍砖讨论,分享自己的心得体会。