通过Iterator控制Promise.all的并发数

更新日期: 2019-09-14阅读: 2.1k标签: 并发

背景

异步是 js 一个非常重要的特性,但很多时候,我们不仅仅想让一系列任务并行执行,还想要控制同时执行的并发数,尤其是在针对操作有限资源的异步任务,比如文件句柄,网络端口等等。

看一个例子。

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// simulate an async work that takes 1s to finish
async function execute(id) {
  console.log(`start work ${id}`);
  await sleep(1000);
  console.log(`work ${id} done`);
}

Promise.all([1, 2, 3, 4, 5, 6, 7, 8, 9].map(execute));

输出结果:

"start work 1"
"start work 2"
"start work 3"
"start work 4"
"start work 5"
"start work 6"
"start work 7"
"start work 8"
"start work 9"
"work 1 done"
"work 2 done"
"work 3 done"
"work 4 done"
"work 5 done"
"work 6 done"
"work 7 done"
"work 8 done"
"work 9 done"

可以看到,所有的 work 都同时开始执行了。

现在,如果我们想让这些 work 每次只执行 2 个,2 个完成之后再继续后面的 2 个,即并发数为 2 应该怎么做呢?


解决方案

控制Promise的生成是关键

我们知道,Promise.all并不会触发Promise的执行,真正触发执行的是创建Promise本身,换句话说,Promise在生成的一瞬间就已经开始执行了!因此,如果要控制Promise的并发,我们就要控制Promise的生成。

通过Iterator控制并发数

常见的解决方案是通过一个函数接收并发任务数组,并发函数,并发数 3 个参数,根据并发数,监控Promise的完成状态,批量创建新的Promise,从而达到控制Promise生成的目的。

现在,我们来尝试另外一个思路,通过Iterator来控制并发数。

同时遍历同一个Iterator会发生什么?

让我们先来看一个简化的例子。

// Array.values returns an Array Iterator
const iterator = [1, 2, 3].values();

for (const x of iterator) {
  console.log(`loop x: ${x}`);

  for (const y of iterator) {
    console.log(`loop y: ${y}`);
  }
}

输出结果:

"loop x: 1"
"loop y: 2"
"loop y: 3"

注意到没有?y 循环接着 x 循环继续,并且 2 个循环都在所有元素遍历完之后结束了!这正是我们要利用的特性。
对 Iterator 不熟悉的同学可以参考 MDN 文章https://developer.mozilla.org...

用Iterator改造 work 的例子

让我们用Iterator的这个特性来改造最开始的 work 例子。

// generate workers according to concurrency number
// each worker takes the same iterator
const limit = concurrency => iterator => {
  const workers = new Array(concurrency);
  return workers.fill(iterator);
};

// run tasks in an iterator one by one
const run = func => async iterator => {
  for (const item of iterator) {
    await func(item);
  }
};

// wrap limit and run together
function asyncTasks(array, func, concurrency = 1) {
  return limit(concurrency)(array.values()).map(run(func));
}

Promise.all(asyncTasks(tasks, execute, 2));

输出结果:

"start work 1"
"start work 2"
"work 1 done"
"start work 3"
"work 2 done"
"start work 4"
"work 3 done"
"start work 5"
"work 4 done"
"start work 6"
"work 5 done"
"start work 7"
"work 6 done"
"start work 8"
"work 7 done"
"start work 9"
"work 8 done"
"work 9 done"

结果和我们预想的一样,每次只同时执行两个异步任务直到所有任务都执行完毕。

不过,这个方案也不是完美无缺。主要问题在于,如果某个 worker 在执行过程中出错了,其余的 worker 并不会因此停止工作。也就是说,上面的例子中,如果 worker 1 出现异常停止了,worker 2 会独自执行剩下所有任务,直到全部完毕。因此,如果想要时刻保持 2 个并发,最简单的方法是给每个execute方法添加catch。

尽管不够完美,将Iterator作为控制Promise创建,也不失为一种简单有效的控制异步并发数的简单方法。

当然,实际项目中,还是尽量避免重复造轮子,p-limitasync-pool甚至bluebird都是简单易用的解决方案。


链接: https://fly63.com/article/detial/6433

PHP和Redis实现在高并发下的抢购及秒杀功能示例详解

抢购、秒杀是平常很常见的场景,面试的时候面试官也经常会问到,比如问你淘宝中的抢购秒杀是怎么实现的等等。抢购、秒杀实现很简单,但是有些问题需要解决,主要针对两个问题:

说一说数据库的并发控制

最近在看Elasticsearch时看到了并发控制,由此看到了新的并发控制方式。不得不说Elasticsearch相较于关系型数据库就是两种理论建立的数据存储体系,当然它们在并发控制上也相差甚远,各有千秋。

PHP-高并发和大流量的解决方案

在互联网时代,并发,高并发通常是指并发访问。也就是在某个时间点,有多少个访问同时到来。 高并发架构相关概念QPS (每秒查询率) : 每秒钟请求或者查询的数量,在互联网领域,指每秒响应请求数

处理高并发的一般思路

今天看见有人聊目前系统有2亿的PV,该如何优化?当我看到这个话题的时候,突然在想自己工作中也遇到了不少高并发的场景了,所以即兴发挥,在这里简单总结和分享下,欢迎指正和补充。

nodejs使用 eventproxy 控制并发

很多网站有并发连接数的限制,所以当请求发送太快的时候会导致返回值为空或报错。 安装依赖 express superagent cheerio eventproxy。新建app.js 抓取所有的url

PHP 并发场景的几种解决方案

在秒杀,抢购等并发场景下,可能会出现超卖的现象,在PHP语言中并没有原生提供并发的解决方案,因此就需要借助其他方式来实现并发控制。列出常见的解决方案有:

并发编程三要素:原子性,有序性,可见性

并发编程三要素:原子性: 一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。有序性: 程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)

nodejs如何解决高并发?

Node可以在不新增额外线程的情况下,依然可以对任务进行并发处理 —— Node.js是单线程的。它通过事件循环(event loop)来实现并发操作,对此,我们应该要充分利用这一点 —— 尽可能的避免阻塞操作

如何利用 JavaScript 实现并发控制

在开发过程中,有时会遇到需要控制任务并发执行数量的需求。例如一个爬虫程序,可以通过限制其并发任务数量来降低请求频率,从而避免由于请求过于频繁被封禁问题的发生。

JavaScript 中如何实现并发控制?

在日常开发过程中,你可能会遇到并发控制的场景,比如控制请求并发数。那么在 JavaScript 中如何实现并发控制呢?在回答这个问题之前,我们来简单介绍一下并发控制。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!