利用 JavaScript 实现并发控制的示例代码


Posted in Javascript onDecember 31, 2020

一、前言

  在开发过程中,有时会遇到需要控制任务并发执行数量的需求。

  例如一个爬虫程序,可以通过限制其并发任务数量来降低请求频率,从而避免由于请求过于频繁被封禁问题的发生。

  接下来,本文介绍如何实现一个并发控制器。

二、示例

const task = timeout => new Promise((resolve) => setTimeout(() => {
  resolve(timeout);
 }, timeout))

 const taskList = [1000, 3000, 200, 1300, 800, 2000];

 async function startNoConcurrentControl() {
  console.time(NO_CONCURRENT_CONTROL_LOG);
  await Promise.all(taskList.map(item => task(item)));
  console.timeEnd(NO_CONCURRENT_CONTROL_LOG);
 }

 startNoConcurrentControl();

  上述示例代码利用 Promise.all 方法模拟6个任务并发执行的场景,执行完所有任务的总耗时为 3000 毫秒。

  下面会采用该示例来验证实现方法的正确性。

三、实现

  由于任务并发执行的数量是有限的,那么就需要一种数据结构来管理不断产生的任务。

  队列的「先进先出」特性可以保证任务并发执行的顺序,在 JavaScript 中可以通过「数组来模拟队列」

class Queue {
  constructor() {
   this._queue = [];
  }

  push(value) {
   return this._queue.push(value);
  }

  shift() {
   return this._queue.shift();
  }

  isEmpty() {
   return this._queue.length === 0;
  }
 }

  对于每一个任务,需要管理其执行函数和参数:

class DelayedTask {
  constructor(resolve, fn, args) {
   this.resolve = resolve;
   this.fn = fn;
   this.args = args;
  }
 }

  接下来实现核心的 TaskPool 类,该类主要用来控制任务的执行:

class TaskPool {
  constructor(size) {
   this.size = size;
   this.queue = new Queue();
  }

  addTask(fn, args) {
   return new Promise((resolve) => {
    this.queue.push(new DelayedTask(resolve, fn, args));
    if (this.size) {
     this.size--;
     const { resolve: taskResole, fn, args } = this.queue.shift();
     taskResole(this.runTask(fn, args));
    }
   })
  }

  pullTask() {
   if (this.queue.isEmpty()) {
    return;
   }

   if (this.size === 0) {
    return;
   }

   this.size++;
   const { resolve, fn, args } = this.queue.shift();
   resolve(this.runTask(fn, args));
  }

  runTask(fn, args) {
   const result = Promise.resolve(fn(...args));

   result.then(() => {
    this.size--;
    this.pullTask();
   }).catch(() => {
    this.size--;
    this.pullTask();
   })

   return result;
  }
 }

TaskPool 包含三个关键方法:

  • addTask: 将新的任务放入队列当中,并触发任务池状态检测,如果当前任务池非满载状态,则从队列中取出任务放入任务池中执行。
  • runTask: 执行当前任务,任务执行完成之后,更新任务池状态,此时触发主动拉取新任务的机制。
  • pullTask: 如果当前队列不为空,且任务池不满载,则主动取出队列中的任务执行。

利用 JavaScript 实现并发控制的示例代码

  接下来,将前面示例的并发数控制为2个:

const cc = new ConcurrentControl(2);

 async function startConcurrentControl() {
  console.time(CONCURRENT_CONTROL_LOG);
  await Promise.all(taskList.map(item => cc.addTask(task, [item])))
  console.timeEnd(CONCURRENT_CONTROL_LOG);
 }

 startConcurrentControl();

  执行流程如下:

利用 JavaScript 实现并发控制的示例代码

  最终执行任务的总耗时为 5000 毫秒。

四、高阶函数优化参数传递

await Promise.all(taskList.map(item => cc.addTask(task, [item])))

  手动传递每个任务的参数的方式显得非常繁琐,这里可以通过「高阶函数实现参数的自动透传」

addTask(fn) {
  return (...args) => {
   return new Promise((resolve) => {
    this.queue.push(new DelayedTask(resolve, fn, args));

    if (this.size) {
     this.size--;
     const { resolve: taskResole, fn: taskFn, args: taskArgs } = this.queue.shift();
     taskResole(this.runTask(taskFn, taskArgs));
    }
   })
  }
 }

改造之后的代码显得简洁了很多:

await Promise.all(taskList.map(cc.addTask(task)))

五、优化出队操作

  数组一般都是基于一块「连续内存」来存储,当调用数组的 shift 方法时,首先是删除头部元素(时间复杂度 O(1)),然后需要将未删除元素左移一位(时间复杂度 O(n)),所以 shift 操作的时间复杂度为 O(n)。

利用 JavaScript 实现并发控制的示例代码

  由于 JavaScript 语言的特性,V8 在实现 JSArray 的时候给出了一种空间和时间权衡的解决方案,在不同的场景下,JSArray 会在 FixedArray 和 HashTable 两种模式间切换。

  在 hashTable 模式下,shift 操作省去了左移的时间复杂度,其时间复杂度可以降低为 O(1),即使如此,shift 仍然是一个耗时的操作。

  在数组元素比较多且需要频繁执行 shift 操作的场景下,可以通过「reverse + pop」的方式优化。

const Benchmark = require('benchmark');
 const suite = new Benchmark.Suite;

 suite.add('shift', function() {
  let count = 10;
  const arr = generateArray(count);
  while (count--) {
   arr.shift();
  }
 })
 .add('reverse + pop', function() {
  let count = 10;
  const arr = generateArray(count);
  arr.reverse();
  while (count--) {
   arr.pop();
  }
 })
 .on('cycle', function(event) {
  console.log(String(event.target));
 })
 .on('complete', function() {
  console.log('Fastest is ' + this.filter('fastest').map('name'));
  console.log('\n')
 })
 .run({
  async: true
 })

通过 benchmark.js 跑出的基准测试数据,可以很容易地看出哪种方式的效率更高:

利用 JavaScript 实现并发控制的示例代码

  回顾之前 Queue 类的实现,由于只有一个数组来存储任务,直接使用 reverse + pop 的方式,必然会影响任务执行的次序。

  这里就需要引入双数组的设计,一个数组负责入队操作,一个数组负责出队操作。

class HighPerformanceQueue {
  constructor() {
   this.q1 = []; // 用于 push 数据
   this.q2 = []; // 用于 shift 数据
  }

  push(value) {
   return this.q1.push(value);
  }

  shift() {
   let q2 = this.q2;
   if (q2.length === 0) {
    const q1 = this.q1;
    if (q1.length === 0) {
     return;
    }
    q2 = this.q2 = q1.reverse();
   }

   return q2.pop();
  }

  isEmpty() {
   if (this.q1.length === 0 && this.q2.length === 0) {
    return true;
   }
   return false;
  }
 }

最后通过基准测试来验证优化的效果:

利用 JavaScript 实现并发控制的示例代码

到此这篇关于利用 JavaScript 实现并发控制的示例代码的文章就介绍到这了,更多相关js 并发控制内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
Ajax,UTF-8还是GB2312 eval 还是execScript
Nov 13 Javascript
jQery使网页在显示器上居中显示适用于任何分辨率
Jun 09 Javascript
node.js中的path.normalize方法使用说明
Dec 08 Javascript
用原生JS对AJAX做简单封装的实例代码
Jul 13 Javascript
鼠标点击input,显示瞬间的边框颜色,对之修改与隐藏实例
Dec 26 Javascript
Angular2监听页面大小变化的解决方法
Oct 09 Javascript
js前端导出Excel的方法
Nov 01 Javascript
React 路由懒加载的几种实现方案
Oct 23 Javascript
vue过滤器用法实例分析
Mar 15 Javascript
微信小程序实现渐入渐出动画效果
Jun 13 Javascript
Vue 事件的$event参数=事件的值案例
Jan 29 Vue.js
JS canvas实现画板和签字板功能
Feb 23 Javascript
jquery自定义组件实例详解
Dec 31 #jQuery
使用AutoJs实现微信抢红包的代码
Dec 31 #Javascript
Vue中inheritAttrs的使用实例详解
Dec 31 #Vue.js
element 动态合并表格的步骤
Dec 31 #Javascript
vue导入.md文件的步骤(markdown转HTML)
Dec 31 #Vue.js
Selenium执行JavaScript脚本的方法示例
Dec 31 #Javascript
javascript实现随机抽奖功能
Dec 30 #Javascript
You might like
全国FM电台频率大全 - 7 吉林省
2020/03/11 无线电
php缩小png图片不损失透明色的解决方法
2013/12/25 PHP
PHP同时连接多个mysql数据库示例代码
2014/03/17 PHP
php管理nginx虚拟主机shell脚本实例
2014/11/19 PHP
jQuery+PHP实现的掷色子抽奖游戏实例
2015/01/04 PHP
document.getElementById介绍
2011/09/13 Javascript
javascript 闭包
2011/09/15 Javascript
深入理解JavaScript系列(39):设计模式之适配器模式详解
2015/03/04 Javascript
jfreechart插件将数据展示成饼状图、柱状图和折线图
2015/04/13 Javascript
jquery插件tytabs.jquery.min.js实现渐变TAB选项卡效果
2015/08/25 Javascript
BootStrap扔进Django里的方法详解
2016/05/13 Javascript
Vue iview-admin框架二级菜单改为三级菜单的方法
2018/07/03 Javascript
如何在Vue.js中实现标签页组件详解
2019/01/02 Javascript
Layui实现带查询条件的分页
2019/07/27 Javascript
中级前端工程师必须要掌握的27个JavaScript 技巧(干货总结)
2019/09/23 Javascript
Vue+Node服务器查询Mongo数据库及页面数据传递操作实例分析
2019/12/20 Javascript
压缩包密码破解示例分享(类似典破解)
2014/01/17 Python
在Python的Django框架中实现Hacker News的一些功能
2015/04/17 Python
python进行两个表格对比的方法
2018/06/27 Python
Python输出\u编码将其转换成中文的实例
2018/12/15 Python
在Pycharm中设置默认自动换行的方法
2019/01/16 Python
python随机在一张图像上截取任意大小图片的方法
2019/01/24 Python
Python定义函数功能与用法实例详解
2019/04/08 Python
Python之指数与E记法的区别详解
2019/11/21 Python
关于numpy数组轴的使用详解
2019/12/05 Python
Python 解决OPEN读文件报错 ,路径以及r的问题
2019/12/19 Python
解决python cv2.imread 读取中文路径的图片返回为None的问题
2020/06/02 Python
python判断正负数方式
2020/06/03 Python
python使用scapy模块实现ARP扫描的过程
2021/01/21 Python
用JAVA实现一种排序,JAVA类实现序列化的方法(二种)
2014/04/23 面试题
2014年小学教学工作总结
2014/11/13 职场文书
2019餐饮行业创业计划书!
2019/06/27 职场文书
导游词之香港-太平山顶
2019/10/18 职场文书
利用html+css实现菜单栏缓慢下拉效果的示例代码
2021/03/30 HTML / CSS
什么是SOLID
2022/03/24 Javascript
Tomcat安装使用及部署Web项目的3种方法汇总
2022/08/14 Servers