试图确定一个大列表中有多少偶数。遵循天真的方法:迭代列表并在找到偶数时增加计数。示例代码:
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);
});
};
事件循环还会被阻塞吗?如果是,如何解除阻塞?
答案 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;这篇文章里面有很多有用的链接。