三个关键点实现并发任务队列

428 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情

题目要求:实现一个任务队列函数,最多并发执行limit个任务

核心逻辑

  1. 依次执行 limit 个任务
  2. 任务完成递归执行下一个任务
  3. 直到所有任务执行完毕

代码实现

function taskQueue(tasks, limit) {
    // 1. 依次执行limit个任务
    for (let index = 0; index < limit; index++) {
        run(tasks.shift(), tasks);
    }
}

function run(task, tasks) {
    new Promise((resolve, reject) => {
        resolve(execute(task).then());
    }).then(() => {
        // 2. 任务完成递归执行下一个任务
        // 3. 直到所有任务执行完毕
        tasks.length && run(tasks.shift(), tasks);
    });
}
function execute(url) {
    return new Promise(resolve => {
        // 模拟异步请求
        setTimeout(() => {
            console.log("任务:" + url + "完成", new Date());
            resolve({ url: url });
        }, 1000);
    });
}
// 测试
taskQueue([1, 2, 3, 4, 5, 6, 7], 3);

打印信息改为document.write("任务:" + url + "完成", new Date()+'<br />'); 我们看一下测试结果:

chrome-capture (6).gif

更进一步

有些面试题还要求按照传入的顺序返回任务结果,那么我们要如何实现呢?

先上代码:

function taskQueue(tasks = [], limit) {
    const res = [];
    let count = 0; //已经执行的任务数量(已执行不代表已返回)
    return new Promise(resolve => {
        for (let i = 0; i < limit; i++) {
            run();
        }
        // 任务总数len
        const len = tasks.length;
        // 执行任务
        function run() {
            // 这里不能用`count++;`代替哦,大家可以想下为什么?
            let current = count++;
            //临界条件
            if (current >= len) {
                res.length === len && resolve(res);
                return;
            }
            console.log("current", current);
            // 发送异步请求
            execute(tasks[current])
                .then(data => {
                    res.push(data);
                    // 如果还有任务没执行,就递归执行任务
                    if (current < len) {
                        run();
                    }
                })
                .catch(err => {
                    res.push(err);
                    // 如果还有任务没执行,就递归执行任务
                    if (current < len) {
                        run();
                    }
                });
        }
    });
}

function execute(url) {
    return new Promise(resolve => {
        // 模拟异步请求
        setTimeout(() => {
            console.log("任务:" + url + "完成", new Date());
            resolve({ url: url });
        }, 1000);
    });
}
// 测试
(async () => {
    const res = await taskQueue([1, 2, 3, 4, 5, 6, 7], 3);
    console.log("res", res);
})();

执行结果:

2022-04-02 20.35.17.gif

讲解

上面其实包含了并行和串行两个步骤, 并行是指首先同时发起limit个异步操作

for (let i = 0; i < limit; i++) {
    run();
}

串行是执行完一个请求之后,紧接着去执行下一个任务:

execute(tasks[current])
    .then(data => {
        res.push(data);
        // 如果还有任务没执行,就递归执行任务
        if (current < len) {
            run();
        }
    })
    .catch(err => {
        res.push(err);
        // 如果还有任务没执行,就递归执行任务
        if (current < len) {
            run();
        }
    });

还有一个关键点就是临界条件的判断,这里用了两层:

//临界条件
if (current >= len) {
    res.length === len && resolve(res);
    return;
}

因为任务是异步的,所以current 很可能一定会出现大于len的情况。

试想在current === 5的时候和 current === 6的时候,都会继续执行 run(), 那么肯定会一前一后导致 current 增加了两次。 由于异步请求的返回时间不确定,这个current最后的大小也不一定哦。当然和limit也有关系。

为了说明这一点,我们不妨把current的打印平移到临界条件判断之前;结果如下:

image.png

所以,我们需要在临界条件里面判断,当前返回结果的长度,如果返回了全部的结果就resolve, 没有的话就再等等,但是不需要继续执行run(),因为current >= len 说明所有任务都已经被执行过了。

优化

可能大家都看到了上面的代码中有一块重复代码很辣眼睛,特别是对于强迫症三级患者的我就更不能忍了。就是then().catch()那部分啦。 类比try catch finally, 是不是也应该有个finally避免成功和失败的冗余逻辑代码呢?

google一下还真有,果然是我少见多怪了。 Promise.prototype.finally()

所以上面的代码精简成了29行!

希望下回再遇到这道题或者类似的题目能够直接套用这种思路,轻松搞定。

function taskQueue(tasks = [], limit) {
    const res = [];
    let count = 0;
    return new Promise(resolve => {
        const len = tasks.length;
        for (let i = 0; i < limit; i++) {
            run();
        }
        function run() {
            let current = count++;
            if (current >= len) {
                res.length === len && resolve(res);
                return;
            }
            execute(tasks[current])
                .then(data => {
                    res.push(data);
                })
                .catch(err => {
                    res.push(err);
                })
                .finally(() => {
                    if (current < len) {
                        run();
                    }
                });
        }
    });
}

总结

  • 限制并发不搜集结果
  1. 依次执行 limit 个任务
  2. 任务完成递归执行下一个任务
  3. 直到所有任务执行完毕
  • 限制并发并按传入顺序搜集返回结果
  1. 依次执行 limit 个任务
  2. 返回一个promise实例
  3. 任务完成递归执行下一个任务
  4. 搜集任务返回结果存入数组
  5. 直到所有任务执行完毕 并 返回结果数 === 任务数
  6. promise执行resolve()

熟练掌握 递归遍历 等编程技巧,轻松应对面试常考题目。

不常用的知识点很容易忘记,可以收藏起来及时复习下,才能活得更长久的记忆。

更文不易,如果对你有所帮助,欢迎点赞评论,这将成为我继续创作的动力,不胜感激。