在 Node.js 中使 CPU 绑定任务异步的最简单方法?

时间:2021-04-17 06:27:14

标签: javascript node.js event-loop

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

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

事件循环还会被阻塞吗?如果是,如何解除阻塞?

1 个答案:

答案 0 :(得分:0)

是的,它会阻塞。 Promise 不是某种魔尘;将它洒在阻塞代码上并不会突然使它成为非阻塞代码。

在您的代码中,new Promise 实际上是多余的:因为您已经将您的函数声明为异步函数,它已经一直返回一个 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;这篇文章里面有很多有用的链接。

相关问题