Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Javascript background loop

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?

My solution

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?

like image 989
grabantot Avatar asked Oct 18 '22 08:10

grabantot


1 Answers

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 awaits.

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="">
like image 165
trincot Avatar answered Oct 20 '22 23:10

trincot