【问题标题】:Javascript background loopJavascript后台循环
【发布时间】:2017-08-06 08:01:25
【问题描述】:

假设我们有一个loop.js 文件:

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
}

如果运行 (node loop.js) 输出:

loop started
read file started
loop finished
loop result processing started
timer fires
file processing started

如何在循环在后台运行时重写此代码以读取和处理文件?

我的解决方案

我想出的是这样的:

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
}

确实记录了:

loop started
read file started
file processing started
timer fires
loop finished
loop result processing started

有没有更简单、更简洁的方法来做到这一点?我的解决方案有什么缺点?

【问题讨论】:

  • 目标平台是单核还是多核?这很重要,因为多核平台可以利用不阻碍事件循环的多进程解决方案。对于单核环境,虽然您的解决方案似乎足够好,但可以使用 process.nextTick 之类的结构 - 所有这些都不可避免地阻碍了事件循环(这也适用于上述代码)。
  • 如果它是单核的,那么你的解决方案是唯一的方法,但我认为它不会像你期望的那样工作,因为它是单线程的,并且一次只能处理 1 个任务时间
  • @ManasJayanth, @marvel308 是的,该平台是单核的(最常见的情况),我完全了解node.js 的单线程性质。你能详细说明process.nextTick吗?我看不出它有什么帮助,因为它将功能添加到事件队列的头部(如您所述,阻止其他任务)。
  • 如果你真的想让for循环逻辑与其他处理并行运行,那么把它放在一个工作进程中(可以是node.js或任何其他技术),然后让两者进程通信。这将允许操作系统真正对它们进行时间切片(无论是单 CPU 还是多 CPU)。
  • 为什么有些人在这里声称核心数量在任何方面都很重要?您似乎不了解多线程的工作原理。实际上,单核和多核系统之间的唯一区别是性能。

标签: javascript node.js asynchronous background-task event-loop


【解决方案1】:

第一个版本的代码阻止进一步处理的原因是await 得到一个立即解析的承诺(值null 被包装在一个承诺中,就像你做了await Promise.resolve(null) 一样)。这意味着await 之后的代码将在当前“任务”期间恢复:它只是在任务队列中推送一个微任务,该微任务将在同一个任务中被消耗。您待处理的所有其他异步内容都在任务队列中等待,而不是在微任务队列中。

setTimeout 是这种情况,readFile 也是如此。它们的回调在任务队列中等待处理,因此不会优先于awaits 生成的微任务。

所以你需要一种方法让await 将某些东西放入任务队列而不是微任务队列。你可以通过向它提供一个不会立即解决的承诺来做到这一点,但只会在当前任务之后解决。

您可以使用 ....setTimeout:

const slowResolve = val => new Promise(resolve => setTimeout(resolve.bind(null, val), 0));

您可以使用await 调用该函数。这里是一个使用图片加载而不是文件加载的sn-p,但是原理是一样的:

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
}
&lt;img id="fs" src=""&gt;

【讨论】:

  • 谢谢。在我的第一个示例中,我将await null 更改为await new Promise(resolve =&gt; setImmediate(resolve))。我知道我很接近了。
  • @grabantot - 在我看来,任何依赖于微任务队列与常规任务队列之间的差异才能正常工作的代码都是脆弱的(当其他人在它上面工作时)并且是其意图不是很明确。 IMO,如果您希望在其他事情之后发生某些事情,那么以适当的延迟来安排它,而不是依赖于任务类型之间的微小差异,然后您的代码即使对于那些不了解细微差别的人来说也是显而易见的在微任务和常规任务之间进行调度。
猜你喜欢
  • 1970-01-01
  • 2011-02-04
  • 2020-06-04
  • 1970-01-01
  • 1970-01-01
  • 2017-04-29
  • 2021-12-10
  • 2019-07-31
  • 2011-12-06
相关资源
最近更新 更多