天天看點

[Effective JavaScript 筆記]第61條:不要阻塞I/O事件隊列

js程式是建構在事件之上的。輸入可能來自不同的外部源。在一些語言中,我們習慣地編寫代碼來等待某個特定的輸入。

var text=downloadSync('http://example.com/file.txt');
console.log(text);
           

像這樣的形式downloadSync稱為同步函數(或阻塞函數)。程式會停止做任何工作,而等待它的輸入。在這個例子中,也就是等待從網絡上下載下傳檔案的結果。由于在等待下載下傳完成的期間,計算機可以做其他有用的工作,是以這樣的語言通常為程式員提供一種方法來建立多個線程,即并行執行子計算。它允許程式的一部分停下來等待(阻塞)一個低速的輸入,而程式的另一部分可以繼續進行獨立的工作。

在js中,大多的I/O操作都提供了異步的或非阻塞的API。程式員提供一個回調函數,一旦輸入完成就可以被系統調用,而不是程式阻塞在等待結果的線程上。

var text=downloadSync('http://example.com/file.txt',function(text){
  console.log(text);
});
           

該API初始化下載下傳進行,然後在内部系統資料庫中存儲了回調函數後立即傳回,而不是被網絡請求阻塞。當下載下傳完成之後,系統會将下載下傳完的檔案的文本作為參數調用該已注冊的回調函數。

随着事件的發生,事件被添加到應用程式的事件隊列的末尾。js系統使用一個内部循環機制來執行應用程式。該循環機制每次都拉取隊列底部的事件,以接收到這些事件的順序來調用這些已經注冊的js事件處理程式,并将事件的資料作為該事件處理程式的參數。

運作到完成機制擔保的好處是當代碼運作時,你完全掌握應用程式的狀态。根本不必擔心一些變量和對象屬性的改變由于并發執行代碼而超出你的控制。并發程式設計在js中往往比使用線程和鎖的c++,java或c#更容易。

然而,運作到完成機制的不足是,實際上所有你編寫的代碼支撐着餘下應用程式的繼續執行。像浏覽器這樣的互動式應用程式中,一個阻塞的事件處理程式會阻塞任何将被處理的其他使用者輸入,甚至可能阻塞一個頁面的渲染,進而導緻頁面失去響應的使用者體驗。在伺服器環境中,一個阻塞的事件處理程式可能會阻塞将被處理的其他網絡請求,進而導緻伺服器失去響應。

js并發的一個最重要的規則是絕不要在應用程式事件隊列中使用阻塞I/O的API。在浏覽器中,甚至幾乎沒有任何阻塞API是可用的,盡管有一些平台實作了。提供類似于downloadAsync功能的網絡I/O的XMLHttpRequest庫有一個同步的版本實作,被認為是不好的。對于web應用程式的互動性,同步的I/O會導緻災難性的後果,它在I/O操作完成之前一直會阻塞使用者與頁面的互動。

相比之下,異步的API用在基于事件的環境中是安全的,它們迫使應用程式邏輯在一個獨立的事件循環“輪詢”中繼續處理。如上面的例子,假設需要幾秒鐘來下載下傳網絡資源,在這段時間裡,數量龐大的其他事件很可能發生。在同步的實作中,這些事件就會堆積在事件隊列中,而事件循環将停留等待該JS代碼執行完成,這将阻塞任何其他事件的處理。在異步的版本中,JS代碼注冊一個事件處理程式并立即傳回,這将在下載下傳完成之前,允許其他處理程式處理這期間的事件。

在主應用程式事件隊列不受影響的環境中,阻塞操作很少出問題。例如,web平台提供了Worker的API,該API使得産生大量的并行計算成為可能。不同于傳統的線程執行,Workers在一個完全隔離的狀态下執行,沒有擷取全局作用域或應用程式主線程web頁面内容的能力。是以,它們不會妨礙主事件隊列中運作的代碼的執行。在一個Worker中,使用XMLHttpRequest同步的變種很少出問題。下載下傳操作的确會阻塞Worker繼續執行,但這并不會阻止頁面的渲染或事件隊列中的事件響應。在伺服器端環境中,阻塞的API在啟動一開始是沒有問題的,也就是在伺服器開始響應輸入的請求之前。然後在處理請求期間,浏覽器事件隊列中存在阻塞的API就是有問題的啦。

提示

  • 異步API使用回調函數來延緩處理代價高昂的操作以避免阻塞主應用程式
  • js并發地接收事件,但會使用一個事件隊列按序地處理事件處理程式
  • 在應用程式事件隊列中絕不要使用阻塞的I/O

版權聲明

翻譯的文章,版權歸原作者所有,隻用于交流與學習的目的。

原創文章,版權歸作者所有,非商業轉載請注明出處,并保留原文的完整連結。

繼續閱讀