Say we have a loop.js
file:
longLoop().then(res => console.log('loop result processing started'))
console.log('read file started')
require('fs').readFile(__filename, () => console.log('file processing started'))
setTimeout(() => console.log('timer fires'), 500)
async function longLoop () {
console.log('loop started')
let res = 0
for (let i = 0; i < 1e7; i++) {
res += Math.sin(i) // arbitrary computation heavy operation
if (i % 1e5 === 0) await null /* solution: await new Promise(resolve => setImmediate(resolve)) */
}
console.log('loop finished')
return res
}
Which if ran (node loop.js
) outputs:
loop started
read file started
loop finished
loop result processing started
timer fires
file processing started
How can this code be rewritten to read and process file while the loop is running in the background?
What I came up with is this:
longLoop().then(res => console.log('loop result processing started'))
console.log('read file started')
require('fs').readFile(__filename, () => console.log('file processing started'))
setTimeout(() => console.log('timer fires'), 500)
async function longLoop () {
let res = 0
let from = 0
let step = 1e5
let numIterations = 1e7
function doIterations() {
//console.log(from)
return new Promise(resolve => {
setImmediate(() => { // or setTimeout
for (let i = from; (i < from + step) && (i < numIterations); i++) {
res += Math.sin(i)
}
resolve()
})
})
}
console.log('loop started')
while (from < numIterations) {
await doIterations()
from += step
}
console.log('loop finished')
return res
}
Which indeed logs:
loop started
read file started
file processing started
timer fires
loop finished
loop result processing started
Is there a simpler, more concise way to do that? What are the drawbacks of my solution?
The reason why the first version of your code blocks further processing is that await
gets an immediately resolving promise (the value null
gets wrapped in a promise, as if you did await Promise.resolve(null)
). That means the code after await
will resume during the current "task": it merely pushes a microtask in the task queue, that will get consumed within the same task. All other asynchronous stuff you have pending is waiting in the task queue, not the microtask queue.
This is the case for setTimeout
, and also for readFile
. Their callbacks are pending in the task queue, and as a consequence will not get priority over the mircrotasks generated by the await
s.
So you need a way to make the await
put something in the task queue instead of the microtask queue. This you can do by providing a promise to it that will not immediately resolve, but only resolves after the current task.
You could introduce that delay with .... setTimeout
:
const slowResolve = val => new Promise(resolve => setTimeout(resolve.bind(null, val), 0));
You would call that function with the await
. Here is a snippet that uses an image load instead of a file load, but the principle is the same:
const slowResolve = val => new Promise(resolve => setTimeout(resolve.bind(null, val), 0));
longLoop().then(res =>
console.log('loop result processing started'))
console.log('read file started')
fs.onload = () =>
console.log('file processing started');
fs.src = "https://images.pexels.com/photos/34950/pexels-photo.jpg?h=350&auto=compress&cs=tinysrgb";
setTimeout(() => console.log('timer fires'), 500)
async function longLoop () {
console.log('loop started')
let res = 0
for (let i = 0; i < 1e7; i++) {
res += Math.sin(i) // arbitrary computation heavy operation
if (i % 1e5 === 0) await slowResolve(i);
}
console.log('loop finished')
return res
}
<img id="fs" src="">
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With