为什么while循环会阻塞事件循环?

IT技术 javascript node.js
2021-02-07 21:50:12

Node.js 书中给出了以下示例:

var open = false;

setTimeout(function() {
  open = true
}, 1000)

while (!open) {
  console.log('wait');
}

console.log('open sesame');

解释为什么 while 循环会阻塞执行,作者说:

Node 永远不会执行超时回调,因为事件循环卡在第 7 行开始的 while 循环中,永远不会给它处理超时事件的机会!

但是,作者没有解释为什么会在事件循环的上下文中发生这种情况,或者幕后真正发生了什么。

有人可以详细说明一下吗?为什么节点会卡住?以及如何更改上述代码,同时保留while控制结构,以便事件循环不会被阻塞,并且代码将按照人们合理预期的方式运行;等待将在setTimeout触发前仅记录 1 秒,然后该进程在记录“芝麻开门”后退出。

通用解释,例如关于 IO 和事件循环和回调的这个问题的答案并不能真正帮助我合理化这一点。我希望直接引用上述代码的答案会有所帮助。

5个回答

这真的很简单。在内部,node.js 由这种类型的循环组成:

  • 从事件队列中获取一些东西
  • 运行任何指示的任务并运行它直到它返回
  • 当上述任务完成后,从事件队列中获取下一项
  • 运行任何指示的任务并运行它直到它返回
  • 冲洗、起泡、重复——一遍又一遍

如果在某个时刻,事件队列中没有任何内容,则进入睡眠状态,直到将某些内容放入事件队列中。


因此,如果一段 Javascript 处于while()循环中,则该任务未完成,并且按照上述顺序,在该先前任务完全完成之前,不会从事件队列中挑选任何新内容。因此,一个很长或永远运行的while()循环只会使工作变得困难。因为 Javascript 一次只运行一项任务(单线程用于 JS 执行),如果该任务在 while 循环中旋转,则其他任何任务都无法执行。

这是一个可能有助于解释它的简单示例:

 var done = false;

 // set a timer for 1 second from now to set done to true
 setTimeout(function() {
      done = true;
 }, 1000);

 // spin wait for the done value to change
 while (!done) { /* do nothing */}

 console.log("finally, the done value changed!");

有些人可能会合乎逻辑地认为 while 循环将旋转直到计时器触发,然后计时器将更改doneto的值,true然后 while 循环将完成,console.log()最后将执行。那不是会发生的事情。这实际上将是一个无限循环,并且console.log()永远不会执行语句。

问题是,一旦您进入while()循环中的自旋等待,就无法执行其他 Javascript。因此,想要更改变量值的计时器done无法执行。因此,while 循环条件永远不会改变,因此它是一个无限循环。

以下是 JS 引擎内部发生的事情:

  1. done 变量初始化为 false
  2. setTimeout() 从现在起 1 秒内安排一个计时器事件
  3. while 循环开始旋转
  4. 进入 while 循环旋转 1 秒后,计时器在内部触发 JS 引擎,并将计时器回调添加到事件队列中。这可能发生在 JS 引擎内部的不同线程上。
  5. while 循环一直在旋转,因为done变量永远不会改变。因为它继续旋转,JS 引擎永远不会完成这个执行线程,也永远不会从事件队列中提取下一个项目。

node.js 是一个事件驱动的环境。为了在现实世界的应用程序中解决这个问题,done标志会在未来的某个事件中改变。因此,while您将在将来为某些相关事件注册一个事件处理程序并在那里完成您的工作,而不是一个旋转循环。在绝对最坏的情况下,您可以设置一个循环计时器和“轮询”来经常检查标志,但几乎在每种情况下,您都可以为实际事件注册一个事件处理程序,这将导致done标志更改并执行你的工作。知道其他代码想知道什么时候发生变化的正确设计的代码甚至可以提供自己的事件侦听器和自己的通知事件,人们可以注册感兴趣的事件,甚至只是一个简单的回调。

可以想象,js 解释器或运行时可以在一个线程上执行此操作。也请原谅我可能糟糕的 js 措辞和术语。我不擅长js。
2021-03-28 21:50:12
所以我绕着灌木丛跑了很长时间,while 循环在 js 中阻塞。
2021-04-03 21:50:12
@marshalcraft - Javascript 块中的任何循环。Javascript 是单线程和事件驱动的,因此只要循环正在运行,除了循环中的代码之外,没有其他任何东西可以运行。而且,这不仅仅是循环。任何长时间运行的函数都会阻止其他东西运行。单线程 - Javascript 一次只能做一件同步的事情。所有循环都是同步的。万一你错过了,总结在我的第三段中,它解释了事件队列的工作原理(这是理解这一切的关键)。我不认为绕着灌木丛跑很长时间。
2021-04-03 21:50:12
谢谢@jfriend00。这是一个很好的答案,正是我一直在寻找的。我希望其他人也能点赞,而不是你需要代表;)
2021-04-09 21:50:12
我向 jftiend 道歉。我太挑剔了。所以非异步函数块。但实际上异步函数中的 while 循环仍然会阻塞。似乎从开始到退出的 while 循环是异步执行的单个单元。我知道js是单线程的。但它不一定内联执行代码,因此在某些情况下它会暂停以继续异步代码或事件。由于它不使用时间来决定执行多少,它必须使用“原子”或基本执行单元。就像运行一点全局范围的代码,暂停处理事件或做一些异步函数的工作。
2021-04-10 21:50:12

这是一个很好的问题,但我找到了解决方法!

var sleep = require('system-sleep')
var done = false

setTimeout(function() {
  done = true
}, 1000)

while (!done) {
  sleep(100)
  console.log('sleeping')
}

console.log('finally, the done value changed!')

我认为它有效,因为system-sleep不是旋转等待。

我后来发现这是唯一有效的解决方案,但它只有效,因为它是 C++ hack,这在 Node.js 中应该是不可能的 - 这将适用于大多数平台,但是,它会在某些平台上失败,所以应谨慎使用 - 但是非常适合开发人员使用
2021-03-16 21:50:12
我想知道除了所有的 PHD 演讲之外,没有人赞成这个页面上唯一有效的解决方案。
2021-04-03 21:50:12

还有另一种解决方案。您几乎可以在每个循环中访问事件循环。

let done = false;

setTimeout(() => {
  done = true
}, 5);

const eventLoopQueue = () => {
  return new Promise(resolve => 
    setImmediate(() => {
      console.log('event loop');
      resolve();
    })
  );
}

const run = async () => {
  while (!done) {
    console.log('loop');
    await eventLoopQueue();
  }
}

run().then(() => console.log('Done'));

Node 是一个单一的串行任务。没有并行性,它的并发是IO绑定的。可以这样想:一切都在单个线程上运行,当您进行阻塞/同步的 IO 调用时,您的进程将停止,直到数据返回;但是,假设我们有一个线程,而不是等待 IO(读取磁盘、抓取 url 等),您的任务会继续执行下一个任务,并在该任务完成后检查该 IO。这基本上就是节点所做的,它是一个“事件循环”,它的轮询 IO 用于在循环中完成(或进度)。因此,当任务未完成(您的循环)时,事件循环不会进行。简而言之。

@codecowboy 在 Node.js 中只有一个主线程。所以所有代码只能由该线程执行。setTimeout将安排在1秒后要执行的功能。所以它不会立即执行函数并移动到下一个可执行语句。它找到一个while循环。由于它是单线程的,只有当当前任务完成时,Node.js 才能从队列中选择下一项。但是随后while无限运行,因此注册的函数setTimeout永远没有机会执行。
2021-03-15 21:50:12

因为计时器需要返回并且正在等待循环完成以添加到队列中,所以虽然超时在一个单独的线程中,并且可能确实完成了计时器,但是设置 done = true 的“任务”正在等待那个无限循环完成