【问题标题】:Easiest way to make a CPU bound task asynchronous in Node.js? [duplicate]在 Node.js 中使 CPU 绑定任务异步的最简单方法? [复制]
【发布时间】:2021-07-12 01:50:21
【问题描述】:

试图确定一个大列表中有多少个偶数。遵循简单的方法:遍历列表并在找到偶数时增加计数。示例代码:

const list = [34, 1, 35, 3, 4, 8]; //This list may become really big. taking more than 3 seconds often

let evenCount = 0;

for (const elem of list) {
  if (elem % 2 === 0) {
    evenCount++;
  }
}
console.log(evenCount);

我知道这会在它执行的整个过程中阻塞事件循环。尝试计算内部承诺,即

const determineEvenCount = async list => {
  return new Promise((resolve, reject) => {
    let evenCount = 0;
    for (const elem of list) {
      if (elem % 2 === 0) {
        evenCount++;
      }
    }
    resolve(evenCount);
  });
};

事件循环还会被阻塞吗?如果是,如何让它畅通无阻?

【问题讨论】:

  • 是的,它会阻塞。您可以使用 webWorkers,但对于如此微不足道的事情,我感觉 IPC 消息传递开销会一样长。不过 3 秒,这个数组有多大,你确定是这个需要那么长时间吗?
  • 你可以分块执行它并等待每个 x 元素或 ms 的简单任务(例如 await new Promise(res => setImmediate(res));)。

标签: javascript node.js event-loop


【解决方案1】:

是的,它会阻塞。承诺不是某种神奇的尘埃;将其洒在阻塞代码上并不会突然使其变为非阻塞。

在你的代码中new Promise 实际上是多余的:因为你已经将你的函数声明为异步,它已经一直返回一个承诺。是的,因为你原来的 Promise Executor 是同步的(你用纯 for ... of 遍历你的列表),你的其余代码必须等到那里的命令队列用完:

const smallerList = Object.keys([...Array(1E4)].map(Number));
const largerList = Object.keys([...Array(1E5)].map(Number));

const determineEvenCount = async (list) => {
  console.time('Inside Loop: ' + list.length);
  let evenCount = 0;
  for (let elem of list) {
    if (elem % 2 === 0) {
      evenCount++;
    }
  }
  console.timeEnd('Inside Loop: ' + list.length);
  console.log(evenCount);
  return evenCount;
};

console.time('Timer execution');
setTimeout(() => {
  console.timeEnd('Timer execution');
}, 5);
Promise.resolve().then(() => { 
  console.log('Microtask execution');
});
console.time('Waiting for sync');
determineEvenCount(largerList);
determineEvenCount(smallerList);
console.timeEnd('Waiting for sync');

如您所见,列表是按顺序迭代的(较大的列表在较小的列表之前,尽管后者显然花费的时间更少),然后计时器和承诺都被触发。完全屏蔽。

不过,有一个非常简单的方法可以让这个函数阻塞:

const smallerList = Object.keys([...Array(1E4)].map(Number));
const largerList = Object.keys([...Array(1E5)].map(Number));

const determineEvenCount = async (list) => {
  console.time('Inside Loop: ' + list.length);
  let evenCount = 0;
  for await (let elem of list) {
    if (elem % 2 === 0) {
      evenCount++;
    }
  }
  console.timeEnd('Inside Loop: ' + list.length);
  console.log(evenCount);
  return evenCount;
};

console.time('Timer execution');
setTimeout(() => {
  console.timeEnd('Timer execution');
}, 5);
Promise.resolve('Microtask execution A').then(console.log);
console.time('Waiting for async');
determineEvenCount(largerList);
determineEvenCount(smallerList);
console.timeEnd('Waiting for async');
Promise.resolve('Microtask execution B').then(console.log);

...结果如下所示:

Waiting for async: 0.075ms
Microtask execution A
Microtask execution B
Inside Loop: 18.555ms
5000
50000
Timer execution: 46.400ms

如您所见,不仅包装计时器立即执行,而且两个 Promise 都已解决您的数组被处理之前。一切都很美好,对吧?

不。我们停留在同一个循环中(感谢@Kaiido 指出了那部分),这意味着不仅整个集合的计时器被阻塞,而且没有其他任务(特别是 I/O 处理)能够执行。然而,处理时间显着增加,因为该列表中每个单独元素的提取被延迟。

这就是为什么您最有可能应该首先对处理进行分块,并使用setImmediate 来延迟每个块。例如(这里使用 setTimeout 来模拟 setImmediate,只是为了展示想法):

const smallerList = Object.keys([...Array(1E4)].map(Number));
const largerList = Object.keys([...Array(1E5)].map(Number));

const setImmediate = (fn) => {
  setTimeout(fn, 0);
};

const determineEvenCount = async (list) => {
  console.time('Inside Loop: ' + list.length);
  return new Promise((resolve) => {
    let evenCount = 0;

    function counter(elem) {
      if (elem % 2 === 0) {
        evenCount++;
      }
    }

    const CHUNK_SIZE = 100;
    ! function processChunk(start, end) {
      const boundary = Math.min(end, list.length);
      let i = start;
      while (i < boundary) {
        counter(list[i++]);
      }
      if (i === list.length) {
        console.timeEnd('Inside Loop: ' + list.length);
        console.log(evenCount);
        return resolve(evenCount);
      }
      setImmediate(() => processChunk(i, i + CHUNK_SIZE));
    }(0, CHUNK_SIZE);

  });
  console.timeEnd('Inside Loop: ' + list.length);
};

console.time('Timer execution');
setTimeout(() => {
  console.timeEnd('Timer execution');
}, 5);
Promise.resolve('Microtask execution A').then(console.log);
console.time('Waiting for async');
determineEvenCount(largerList);
determineEvenCount(smallerList);
console.timeEnd('Waiting for async');
Promise.resolve('Microtask execution B').then(console.log);

...如果您不想进入工人领域,只是将这个处理从主循环中分出(这通常是处理这个问题的最佳方式)。

最后,some food for thought;这篇文章里面有很多有用的链接。

【讨论】:

  • 您的“不可阻挡”版本仍然非常阻挡。它只会将阻塞延迟到下一个微任务,但它仍然会以同样的方式阻塞事件循环。将console.time("nextTask");setTimeout(() =&gt; { console.timeEnd( "nextTask" ); } ); 之间的所有代码包装起来。在这个函数完成之前,我们的下一个任务不会被调用。
  • @Kaiido 你说得对;只有微任务不会在这里被阻止。更新了答案以指出这一点,添加了基于块的非阻塞实现。
猜你喜欢
  • 2021-08-30
  • 2017-04-24
  • 2012-05-20
  • 1970-01-01
  • 2012-06-04
  • 1970-01-01
  • 2016-10-24
  • 1970-01-01
相关资源
最近更新 更多