簡體   English   中英

為什么while循環會阻塞事件循環?

[英]Why does a while loop block the event loop?

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觸發之前,wait 將僅記錄 1 秒,然后在記錄“open sesame”后退出進程。

通用解釋,例如關於 IO 和事件循環和回調的這個問題的答案並不能真正幫助我合理化這一點。 我希望直接引用上述代碼的答案會有所幫助。

這真的很簡單。 在內部,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 循環會一直旋轉,直到計時器觸發,然后計時器會將done的值更改為true ,然后 while 循環將完成,最后的console.log()將執行。 那不是會發生的事情。 這實際上是一個無限循環,並且永遠不會執行console.log()語句。

問題是,一旦您進入while()循環中的自旋等待,就無法執行其他 Javascript。 因此,想要更改done變量值的計時器無法執行。 因此,while 循環條件永遠不會改變,因此它是一個無限循環。

以下是 JS 引擎內部發生的情況:

  1. done變量初始化為false
  2. setTimeout()從現在開始安排一個定時器事件 1 秒
  3. while 循環開始旋轉
  4. 在 while 循環旋轉 1 秒后,計時器已准備好觸發,但在解釋器返回事件循環之前,它實際上無法執行任何操作
  5. while 循環一直在旋轉,因為done變量永遠不會改變。 因為它繼續旋轉,所以 JS 引擎永遠不會完成這個執行線程,也永遠不會從事件隊列中拉下一個項目或運行掛起的計時器。

node.js 是一個事件驅動的環境。 為了在現實世界的應用程序中解決這個問題, done標志將在未來的某個事件中改變。 因此,您將在未來為某些相關事件注冊一個事件處理程序,而不是一個旋轉的while循環,並在那里完成您的工作。 在絕對最壞的情況下,您可以設置一個循環計時器和“輪詢”來經常檢查標志,但在幾乎每一種情況下,您都可以為實際事件注冊一個事件處理程序,這將導致done標志發生變化和做你的工作。 正確設計的代碼知道其他代碼想知道什么時候發生了變化,甚至可以提供自己的事件偵聽器和自己的通知事件,人們可以注冊感興趣,甚至只是一個簡單的回調。

這是一個很好的問題,但我找到了解決辦法!

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不是旋轉等待。

還有另一種解決方案。 幾乎每個周期都可以訪問事件循環。

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'));

節點是一個單一的串行任務。 沒有並行性,它的並發是 IO 綁定的。 可以這樣想:一切都在單個線程上運行,當您進行阻塞/同步的 IO 調用時,您的進程會暫停,直到返回數據; 但是,假設我們有一個線程,而不是等待 IO(讀取磁盤、獲取 url 等),您的任務會繼續執行下一個任務,並在該任務完成后檢查該 IO。 這基本上就是節點所做的,它是一個“事件循環”,它在循環上輪詢 IO 以完成(或進度)。 因此,當任務未完成(您的循環)時,事件循環不會進行。 簡而言之。

因為計時器需要返回並且正在等待循環完成添加到隊列中,所以雖然超時在一個單獨的線程中,並且可能確實完成了計時器,但是設置 done = true 的“任務”正在等待那個無限循環完成

var open = false;
const EventEmitter = require("events");
const eventEmitter = new EventEmitter();

setTimeout(function () {
  open = true;
  eventEmitter.emit("open_var_changed");
}, 1000);

let wait_interval = setInterval(() => {
console.log("waiting");
}, 100);

eventEmitter.on("open_var_changed", () => {
clearInterval(wait_interval);
console.log("open var changed to  ", open);
});

這個例子有效,您可以執行 setInterval 並檢查其中的 open 值是否發生了變化,它會起作用

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM