控制并发

1. 什么是并发

在我们接触计算机中的线程时会遇到个名词:并发。学过操作系统的朋友对这个哥们可能并不陌生,我们在聊如何控制并发之前,先来聊一聊并发是什么?

并发:指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。

上面的话可能太官方了,接下来我们用一个简单比喻来形容一下:比如你现在有两瓶饮料,现在我要你用喝一瓶的时间去喝完两瓶,这个时候你可以往两瓶里面分别插一根吸管,然后一起吸饮料,这样就能同时喝两瓶饮料了。

控制并发应用场景

我们在简单了解了并发是什么东西后,我们可以知道并发这个东西可以提高执行效率,但是天下并没有免费的午餐。我们可以想象一个场景,如果前端在很短的时间内发送几十个请求的话,那就可能会让服务器过载或者客户端性能下降,毕竟机器也不是说无上限的,这个时候我们就可以控制一段时间内的并发数量从而缓解服务器的压力。

2. 实现控制并发

在上文中我们谈到了控制并发用在什么时候,接下来我们用js来简单模拟一下控制并发。

思路:我们需要有一个任务队列来存放需要完成的任务,并且能够自己来控制一段时间内能够并发的任务个数。当并发任务个数满了的时候,如果其中有一个任务完成,那么就将其从并发的任务队列中拿出并且从等待的任务队列中取一个新的任务放入到并发任务中去。

任务的创建

首先我们先给一个函数ajax(time),这个函数我们将其当成需要耗费时间执行的进程。除此之外还有一个函数addTask(time, name)用来将任务添加到任务队列中:

js 复制代码
// 参数用来模拟任务执行时间
function ajax(time) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve();
    }, time);
  });
}
// time:任务执行时间,name:哪个任务
function addTask(time, name) {
  limit
  .add(() => ajax(time))
  .then(() => {
    console.log(`任务 ${name} 完成`); 
  })
}

控制并发数

接下来我们来使用class来实现一下控制并发这个功能,在这个类中我们需要完成以下几个功能:

  1. 拥有任务队列,并且能控制并发任务个数
  2. 可以将任务添加到任务队列
  3. 可以执行任务队列中的任务
js 复制代码
class Limit {
  // 默认控制 2 个并发
  constructor(paralleCount = 2) {
    this.tasks = [] // 装所有任务的任务队列
    this.runningCount = 0 // 正在运行任务个数
    this.paralleCount = paralleCount // 并发量
  }
  
  add(task) {} // 添加任务
  
  _run() {} // 执行任务
}

const limit = new Limit(2); // 同时2个并发

添加到任务队列

接下来我们来完善一下Limitadd函数的逻辑,在这个函数中我们需要完成以下几点:

  1. 返回一个promise对象,方便后续then()的执行
  2. 每次执行都会将任务添加到任务队列,然后执行该任务
js 复制代码
  add(task) { // 添加任务
    return new Promise((resolve, reject) => {
      this.tasks.push({
        task,
        resolve,
        reject
      })
      this._run()
    })
  }

执行任务

在这个类中的_run()函数我们用来执行任务队列中的任务,接下来我们来简单聊聊它的几个注意点:

  1. 如果并发量没有达到上限那么就可以从任务队列中取出任务执行
  2. 如果达到了并发量,那就要等到正在并发中的某一个或者所有任务执行完成后再从任务队列中取出新的任务放入执行
  3. 解构出来的resolve是add函数return出来的promise的

tip:当我们有任务完成之后,就需要从等待任务队列中再拿一个任务出来执行,这时候还是重复执行_run(),这时候我们可以用递归。

js 复制代码
_run() { // 执行任务
    while (this.runningCount < this.paralleCount && this.tasks.length) {
      const { task, resolve, reject } = this.tasks.shift() // 从等待任务队列头部拿出任务执行
      this.runningCount++
      task().then(resolve, reject).finally(() => { // 一个任务完毕了最终都会走下面的代码
        this.runningCount--
        this._run() // 并发任务结束了后,将等待的任务队列中的任务取出执行
      })
    }
  }

完整代码

js 复制代码
function ajax(time) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve();
    }, time);
  });
}


// 任务队列中存储的任务
// [
//   {
//     () => ajax(time), 
//     resolve,
//     reject
//   }, 
// ]
class Limit {
  // 控制并发数量默认为2
  constructor(paralleCount = 2) {
    this.tasks = [] // 装所有任务
    this.runningCount = 0 // 正在运行任务个数
    this.paralleCount = paralleCount // 并发量
  }
  add(task) { // 添加任务
    return new Promise((resolve, reject) => {
      this.tasks.push({
        task,
        resolve,
        reject
      })
   
      this._run()
    })
  }
  _run() { // 执行任务
    while (this.runningCount < this.paralleCount && this.tasks.length) {
      const { task, resolve, reject } = this.tasks.shift() // 从头部取值
      this.runningCount++
      task().then(resolve, reject).finally(() => { // 一个任务完毕了最终都会走下面的代码
        this.runningCount--
        this._run()
      })
    }
  }
}

const limit = new Limit(2); // 同时2个并发
function addTask(time, name) {
  limit
  .add(() => ajax(time))
  .then(() => {
    console.log(`任务 ${name} 完成`); 
  }).catch(() => {
    console.log(`任务 ${name} 失败`);
  });
}

addTask(10000, 1);
addTask(5000, 2);
addTask(1000, 3);
addTask(3000, 4);
addTask(7000, 5);

执行过程

3. setTimeout

在上面的代码中,有同学可能会有疑惑setTimeout不是放在宏任务队列中吗?为什么会两个setTimeout一起开始计时并且执行呢?

在浏览器中,所有的计时器全部归浏览器去处理,所以在浏览器眼里,不管有多少个计时器,都是走同一套倒计时。定时器确实都是要放在宏任务队列中,当浏览器倒计时到了某一个点的时候,只要V8引擎的主线程现在是空着的,它就会把宏任务队列当中的定时器拿出来执行倒计时,到另外一个点的时候,如果此时引擎还是空的,那么它就会把另一个定时器拿出来执行。

总结

  1. 保证所有的任务都受控制(给任务包裹一层函数)
  2. 创建添加任务的 add 函数,并将 add 的 resolve,reject同任务一起存放
  3. 创建执行任务的 run 函数,在每个任务执行完毕后,执行 add 函数的resolve,reject,最后递归执行 run 函数
相关推荐
GISer_Jing1 小时前
前端性能指标及优化策略——从加载、渲染和交互阶段分别解读详解并以Webpack+Vue项目为例进行解读
前端·javascript·vue
不知几秋1 小时前
数字取证-内存取证(volatility)
java·linux·前端
水银嘻嘻2 小时前
08 web 自动化之 PO 设计模式详解
前端·自动化
Zero1017134 小时前
【详解pnpm、npm、yarn区别】
前端·react.js·前端框架
&白帝&4 小时前
vue右键显示菜单
前端·javascript·vue.js
Wannaer4 小时前
从 Vue3 回望 Vue2:事件总线的前世今生
前端·javascript·vue.js
羽球知道5 小时前
在Spark搭建YARN
前端·javascript·ajax
光影少年5 小时前
vue中,created和mounted两个钩子之间调用时差值受什么影响
前端·javascript·vue.js
青苔猿猿5 小时前
node版本.node版本、npm版本和pnpm版本对应
前端·npm·node.js·pnpm
一只码代码的章鱼6 小时前
Spring的 @Validate注解详细分析
前端·spring boot·算法