js异步任务并发数量控制的另外两种实现

345 阅读3分钟

上一篇《如何控制多个异步任务执行的最大并发数》,然后看到另一篇《js异步并发控制,限制请求数量的解惑》,其中的两种实现思路挺好的,值得学习。在此我对这两种实现做了一些代码优化,分享出来。

一、基于执行池+异步函数的实现

思路如下:

  1. 创建一个执行池。池内任务数量最大为limit个
  2. 当执行池中有任务完成后,获取一个任务,放入执行池中执行
  3. 当执行池中所有任务都执行完成,获取结果

上代码


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 }

说明:

  1. 模拟任务的数据结构有变化,任务的结果统一到Promise的fulfilled,方便后续使用Promise.race, Promise.all时不用在try catch。
  2. 任务池executing中,如果有任务完成,如何获得到通知?使用Promise.race。这里选择Set数据结构,方便移除已完成的任务。
  3. 异步函数,for。用同步的编码方式写异步,没有callback回调,提高可读性。
  4. 代码中已有注释,无需在详细解释。

二、通过Promise.all+“包裹Promise”对象+延迟任务上下文队列来实现

思路如下:

  1. 如果用Promise.all(allTasks),那么所有任务都会立即执行,怎么办?创建“包裹Promise对象” (先用这个词描述)列表。
  2. 如何将实际任务和包裹对象对应起来?创建一个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 }

说明:

  1. 代码中已有注释。

关于对引用文中优化了哪些地方,大家可以自行对比,都在代码中。

js关于异步编程中,代码可读性排序:async > yield > Promise then > callback

最后来个个人感受总结:

  1. 上一篇是callback+直接想法的实现
  2. 本篇实现一,通过“同步顺序代码”的方式编写异步。后面准备用另外的题目写一篇。
  3. 本篇实现二,将Promise的resolve传递。此技巧可以用来解决一些时序控制问题等,后续准备另写一篇文章。

欢迎大家拍砖讨论,分享自己的心得体会。