經過Iterator控制Promise.all的併發數

背景

異步是 js 一個很是重要的特性,但不少時候,咱們不單單想讓一系列任務並行執行,還想要控制同時執行的併發數,尤爲是在針對操做有限資源的異步任務,好比文件句柄,網絡端口等等。npm

看一個例子。數組

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的生成。async

經過Iterator控制併發數

常見的解決方案是經過一個函數接收併發任務數組併發函數併發數 3 個參數,根據併發數,監控Promise的完成狀態,批量建立新的Promise,從而達到控制Promise生成的目的。ide

如今,咱們來嘗試另一個思路,經過Iterator來控制併發數。函數

同時遍歷同一個Iterator會發生什麼?

讓咱們先來看一個簡化的例子。oop

// 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}`);
  }
}

輸出結果:ui

"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都是簡單易用的解決方案。

相關文章
相關標籤/搜索