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

背景

异步是 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都是简单易用的解决方案。

原文链接:segmentfault.com

上一篇:你的情绪,会影响币价吗?
下一篇:区块链开发技术引用于军事管理上

相关推荐

  • 🔥 Promise|async|Generator 实现&原理大解析 | 9k字

    笔者刚接触async/await时,就被其暂停执行的特性吸引了,心想在没有原生API支持的情况下,await居然能挂起当前方法,实现暂停执行,我感到十分好奇。好奇心驱使我一层一层剥开有关JS异步编程的...

    4 个月前
  • 面试题1:Promise递归实现拉取数据

    题目:请用promise递归实现拉取100条数据,每次拉取20条,结束条件为当次拉取不足20条或者已经拉取100条数据 ...

    3 个月前
  • 面试官要求我们手动实现 Promise.all

    情景: 最近面试,有两次被问到手动实现 Promise.all,不幸的是我都没把这题做好。因为我没有去准备这个,我不知道手动实现已有的 API 有什么意义。 但是为了防止以后还会遇到此类题,还是记录下...

    2 个月前
  • 面试官你来,130行带你手写完整Promise

    大家好,我是雷锋蜀黍。一直在某些面试宝典看到面试官要你手撸一个promise,今天天气这么好,不如我们来搞一搞。(PS:从未看过别人的实现,本文更像是记录一个思考的过程) 最终的代码完全符合Promi...

    4 个月前
  • 需要ES6中的Iterator迭代器?

    本文原文链接专门说一说Iterator迭代器的原因是,为后面async/await应用原理的文章做铺垫,因为async/await是由GeneratorPromise共同构成,而其中的Generato...

    2 个月前
  • 零代码深入浅出React并发模式,带你理解React Fiber架构

    React Fiber架构有一定的复杂度,如果硬着头皮去啃源码,我们会深陷于庞大的代码量和实现细节之中,往往学不到什么东西。 React并发模式是ReactFiber架构的重要应用,本文不贴任何Rea...

    6 个月前
  • 零代码深入浅出React并发模式

    本文不贴任何React源码,纯粹使用文字讲述React并发模式的原理。 这不是一篇关于React并发模式API使用的文章。 为节约篇幅,下文不会详细介绍API的用法,只会讲解原理。

    6 个月前
  • 阅读Promise A+规范

    本文主要是PromiseA规范(https://promisesaplus.com/)的翻译加上个人的理解。 1 什么是Promise A promise represents the eve...

    2 年前
  • 针对前端开发可重用组件并发布到NPM

    翻译:疯狂的技术宅 原文:https://www.smashingmagazine....(https://www.smashingmagazine.com/2018/07/reusablecom...

    1 年前
  • 通过koa2和Promise.race()构造一个超时取消的ajax。

    MDN上说: 你可以使用AbortController.AbortController()构造函数创建一个新的AbortController对象。 使用AbortSignal 对象完成与DOM请求...

    2 年前

官方社区

扫码加入 JavaScript 社区