我是这样实现并发任务控制的

446 阅读1分钟

本文正在参加「金石计划」

尽管js是一门单线程的脚本语言,其同步代码我们是自上而下读取执行的,我们无法干涉其执行顺序,但是我们可以借助异步代码中的微任务队列来实现任务的并发任务控制。那我们用一个例子来带入一下。

如何使下面的代码按照我所想的效果来输出

function timeout(time) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve()
    }, time)
  })
}

function addTask(time, name) {
  superTask
    .add(() => timeout(time)) 
    .then(() => {
      console.log(`任务${name}完成`);
    })
}

addTask(10000, 1) // 10000   3
addTask(5000, 2)  // 5000    1
addTask(3000, 3)  // 8000    2
addTask(4000, 4)  // 12000   4
addTask(5000, 5)  // 15000   5

就是使得两个任务并发执行,当一个任务执行完了,下一个任务就接在空出来的任务队列中,以此类推。 在代码中我们可以看到在add方法调用后接了.then那么说明我们必须在add函数执行完返回出一个Promise对象

我们在实现并发任务控制之前需要明确并发几个任务执行我们应该可以人工控制

class SuperTask {
  constructor(executeCount = 2) {
    this.executeCount = executeCount;  // 并发执行的任务数
    this.runningCount = 0; // 正在执行的任务数
    this.tasks = [];  // 任务队列
  }
  
  // 添加任务
  add(task) {
    return new Promise((resolve,reject) => {
      this.tasks.push({
        task,
        resolve,
        reject
      });  // add方法将任务添加到任务队列中,后面我会解释为什么需要这么做
      
      /*接下来就是判断正在执行的任务,是不是达到了并发任务数量*/
       this._run();  // 为了代码的可读性,将这个另外写一个方法
    })
  }
  
  // 执行任务队列中的任务
  _run() {
    // 如果正在执行的任务数小于并发执行的任务数,那么我们就将任务队列队头的元素取出来执行
    if(this.tasks.length && this.runningCount < this.executeCount) {
      //任务队列中有任务, 并发任务队列有空余,可以执行任务
      this.runningCount++;
      const { task,resolve,reject } = this.tasks.shift();
      task().then(resolve,reject).finally(res => {
        this.runningCount--;
        this._run();
      })
    }
  }
}

利用Promise.then微任务的特性,我们可以控制Promise的状态改变时,再去取任务队列中队头元素再执行,这个地方有一些细节,就是add函数执行时,返回出的是一个Promise对象,但是我们需要将他resolve以及reject保存下来,使得所有的任务并不是共用同一个Promsie对象,而是每一个任务执行完都是独立的Promise,所以我们需要将其的resolve、reject保存下来,当任务并发执行的队列调用时再使用,再每个task执行完之后的.then之后就说明这个任务已经执行完了,此时this.runningCount--;再递归调用_run函数,考虑代码的完整性,我们使用Promise.finally来执行后续任务,Promise.finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。

代码测试

const superTask = new SuperTask(2)  // 并发执行两个任务

function addTask(time, name) {
  superTask
    .add(() => timeout(time))
    .then(() => {
      console.log(`任务${name}完成`);
    })
}

addTask(10000, 1) // 10000   3
addTask(5000, 2)  // 5000    1
addTask(3000, 3)  // 8000    2
addTask(4000, 4)  // 12000   4
addTask(5000, 5)  // 15000   5
image.png

个人见解

在js中实现并发任务控制,其核心在于借助我们可控制的Promise的状态何时改变,在其状态改变时,说明之前在执行的任务已经执行完毕或者报错了,那么就应该执行后一个任务,但是如果只单纯使用Promsie.then去调用的话,如果其中某一个函数报错,那后续的就不会执行了,所以我们使用Promsie.finally来递归调用,为了保证每一个任务不会相互受影响,我们将其resolve和reject一并保存,在其执行时使用保存的resolve和reject,在后面接.finally这样就实现了。以上就是我个人对于并发任务的想法与见解,当然方式有各种各样,欢迎大家留言讨论。