JS实现并发任务调度,while和setInterval精度对比

117 阅读3分钟

最近看到一道手写代码题:js并发任务调度实现,最大并发数为2;顺手实现了一下,分享自己的解题思路。

先看题目:

function timeout(time) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve()
        }, time)
    })
}
function addTask(time, name) {
    superTask
        .add(() => timeout(time))
        .then(() => {
            console.log(`任务${name}完成`)
        })
}
const superTask = new SuperTask()
addTask(10000, 1) // 10000ms后输出 任务1完成
addTask(5000, 2) // 5000ms后输出 任务2完成
addTask(3000, 3) // 8000ms后输出 任务3完成
addTask(4000, 4) // 12000ms后输出 任务4完成
addTask(5000, 5) // 15000ms后输出 任务5完成

拿到题目先别动手,来拆解一下已知信息:

  1. 首先声明了两个函数,一个timeout函数,一个addTask函数;以及一个基于SuperTask构造出来的实例;
  2. timeout函数接收一个参数,返回promise,接收的参数传给setTimeOut用于定义该promise内部何时执行resolve,所以,timeout用于构建一个用于自定义resolve的promise;
  3. addTask函数接收两个参数,timename,首先调用superTask实例.add,传入一个箭头函数,箭头函数内部将参数time传入timeOut函数并执行,接着调用了.then(推导出.add返回一个Promise实例),最终打印传入的参数name;
  4. 最后执行输出: 2-3-1-4-5

分析之后,得出结论,我们需要实现SuperTask类,并且它具有一个返回promise的add方法,同时在add方法内,需要用一个队列收集添加的执行函数,同时要确保执行的最大并发数。

梳理执行逻辑:

于是我们拿到单次任务调度的执行过程:

image.png

由于每次任务调度都执行了.add方法,于是我们知道,.add接管了所有任务调度的执行:

image.png

实现过程:

  • 实现任务的收集
class SuperTask {
    constructor() {
        this.taskList = [] // 任务队列
        this.excuteNum = 0 // 当前执行任务数
        this.taskExcuteMax = 2 // 最大任务调度数
    }
    /**
     * @param: 返回值为promise的函数
     * @return 返回promise
     */
    add(promiseCallback) {
        return new Promise((resolve) => {
            this.taskList.push({
                executeFn: promiseCallback,
                callbackFn: resolve,
            })
        })
    }
}

此时打印superTask.taskList,我们发现,通过superTask.add的任务已经都被收集了,包括timeout箭头函数,以及.add返回的promise的resolve回调:

image.png

  • 支持调度任务批次执行
class SuperTask {
    constructor() {
        this.taskList = [] // 任务队列
        this.excuteNum = 0 // 当前执行任务数
        this.taskExcuteMax = 2 // 最大任务调度数
        this.excuteItv = null
        this.run() // 初始化时执行一次调度器
    }
    /**
     * @param: 返回值为promise的函数
     * @return 返回promise
     */
    add(promiseCallback) {
        return new Promise((resolve) => {
            this.taskList.push({
                executeFn: promiseCallback,
                resolveFn: resolve,
            })
            this.run()
        })
    }
    run() {
        this.excuteItv = setInterval(() => {
            if (this.taskList && this.excuteNum < this.taskExcuteMax) {
                const executeTask = this.taskList.shift() // 从队头开始取调度函数执行
                this.excuteNum++
                executeTask.executeFn().then(() => {
                    executeTask.resolveFn()
                    this.excuteNum--
                })
            }
            if (this.taskList.length === 0) {
                clearInterval(this.excuteItv)
            }
        }, 0)
    }
}

测试一下我们的功能代码:

image.png 可以,通过,如果想更清晰的知道执行的时间,我们可以加以下代码:

let timer = 0
let globalTimer = setInterval(() => {
    timer += 1
    console.log(timer)
    if (timer > 16) {
        clearInterval(globalTimer)
    }
}, 1000)
addTask(10000, 1) // 10000ms后输出 任务1完成
addTask(5000, 2) // 5000ms后输出 任务2完成
addTask(3000, 3) // 8000ms后输出 任务3完成
addTask(4000, 4) // 11000ms后输出 任务4完成
addTask(5000, 5) // 15000ms后输出 任务5完成

image.png 完美验证。

思考:while与setInterval

由于看到有网友用while执行调度,那么用while的方式处理对比setInterval如何呢,由于while是同步阻塞执行,是否会影响后面定时器函数的调用精度?

带着这样的思考,我又用while实现了一下任务调度逻辑:

// 增加whileRun函数
whileRun() {
    while (this.taskList.length && this.excuteNum < this.taskExcuteMax) {
        const executeTask = this.taskList.shift()
        this.excuteNum++
        executeTask.executeFn().then(() => {
            executeTask.resolveFn()
            this.excuteNum--
        })
    }
}

调用了一下打印结果,相比之下无差别:

image.png

也许是代码复杂度不够高,目前看不出这两者的差异; 不过相比while的执行方式,setInterval可以更加灵活自由的决定其run函数的调用位置,而while则要保证是在每次push-taskList之后调用.