如何控制多个异步任务执行的最大并发数

819 阅读3分钟

问题背景

看到一篇《# 面试官:假如有几十个请求,如何去控制并发?》文章,加上最近也考虑过这样问题,于是尝试实现一下。

先简单描述下需求:假设有100个接口请求,如何控制当前执行的请求任务最大为5个,即并发执行有最大限制5个,等所有任务执行完成后,获取结果。

我们都知道Promise.all是并发执行了所有任务,那么上面需求就需要借助三方库或自己实现了。

大概思路如下:

  1. 有一个任务队列,先从队头取limit个任务,然后并发执行。
  2. 等某任务执行完成后,在从队头取一个任务执行。
  3. 重复步骤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好,测试到位。

提问与思考

  1. 上面示例传入了一个任务数组,在任务执行的过程中,如何动态添加任务?
  2. 示例中,并发数量固定为limit,在任务执行过程中,如何动态修改最大并发数量?
  3. 请将上面示例,重构为面向对象编程。比如定义一个TaskExecutor类,包括动态添加任务方法AddTasks,动态修改最大并发数changeLimitCount,任务执行完成的事件,所有任务执行完成后事件等
  4. 小提问,rests length === 0 这行log会打印多少次,为什么?onComplete函数的第一行判断严谨吗,为什么?

以上,抛砖引玉,若有问题,欢迎指出,谢谢。欢迎大家讨论。

抛个问题:提出问题重要还是解决问题重要?哈哈,不是要解决提出问题的人哈~你怎么看?

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