開始使用Web Workers

jobbole發表於2012-11-28

  單執行緒(Single-threaded)執行是JavaScript語言的設計目標之一,進而言之是保持JavaScript的簡單。但是我必須要說,儘管JavaScript具有如此語言特質,但它絕不簡單!我們所說的“單執行緒”是指JavaScript只有一個執行緒控制。是的,這點令人沮喪,JavaScript引擎一次只能做一件事。

“web workers處在一個嚴格的無DOM訪問的環境裡,因為DOM是非執行緒安全的。”

  現在,你是不是覺得要想利用下你機器閒置的多核處理器太受限制?不用擔心,HTML5將改變這一切。

  JavaScript的單執行緒模式

  有學派認為JavaScript的單執行緒特質是一種簡化,然而也有人認為這是一種限制。後者提出的是一個很好的觀點,尤其是現在web應用程式大量的使用JavaScript來處理介面事件、輪詢服務端介面、處理大量的資料以及基於服務端的響應操作DOM。

  在維護響應式介面的同時,通過單執行緒控制處理如此多事件是項艱鉅的任務。它迫使開發人員不得不依靠一些技巧或採用變通的方法(如使用setTimeout(),setInterval(),或呼叫XMLHttpRequest和DOM事件)來實現併發。然而,儘管這些技巧毫無疑問地提供瞭解決非同步呼叫的方法,但非阻塞的並不意味著是併發的。John Resig在他的部落格中解釋了為什麼不能並行執行。

  限制

  如果你已經和JavaScript打過一段時間的交道,那麼你一定也遭遇過如下令人討厭的對話方塊,提示你有指令碼無響應。沒錯,幾乎大多數的頁面無響應都是由JavaScript程式碼引起的。

  以下是一些執行指令碼時造成瀏覽器無響應的原因:

  • 過多的DOM操作:DOM操作也許是在JavaScript執行中代價最高的。所以,大批量的DOM操作無疑是你程式碼重構的最佳方向之一。
  • 無終止迴圈:審視你程式碼中複雜的巢狀迴圈永遠不是壞事。複雜的巢狀迴圈所做的工作常常比實際需要做的多很多,也許你可以找到其他方法來實現同樣的功能。
  • 同時包含以上兩種:最壞的情況就是明明有更優雅的辦法,卻還是在迴圈中不斷更新DOM元素,比如可以採用DocumentFragment

  好幫手Web Workers

  幸好有了HTML5和Web Workers,你可以真正生成一條非同步的執行緒。當主執行緒處理介面事件時,新的worker可以在後臺執行,它甚至可以有力的處理大量的資料。例如,一個worker可以處理大型的資料結構(如JSON),從中提取變數資訊然後在介面中顯示。好了,廢話不多說,讓我們看一些實際的程式碼吧。

  建立一個Worker

  通常,與web worker相關的程式碼都放在一個獨立的JavaScript檔案中。父執行緒通過在Worker建構函式中指定一個JavaScript檔案的連結來建立一個新的worker,它會非同步載入並執行這個JavaScript檔案。

var primeWorker = new Worker('prime.js');

  啟動Worker

  要啟動一個Worker,則父執行緒向worker傳遞一個資訊,如下所示:

var current = $('#prime').attr('value');
primeWorker.postMessage(current);

  父頁面可以通過postMessage介面與worker進行通訊,這也是跨源通訊(cross-origin messaging)的一種方式。通過postMessage介面除了可以向worker傳遞私有資料型別,它還支援JSON資料結構。但是,你不能傳遞函式,因為函式也許會包含對潛在DOM的引用。

“父執行緒和worker執行緒有它們各自的獨立空間,資訊主要是來回交換而不是共享。”

  資訊在後臺執行時,先在worker端序列化,然後在接收端反序列化。鑑於此,不推薦向worker傳送大量的資料。

  父執行緒同樣可以宣告一個回撥函式,來偵聽worker完成任務後發回的訊息。這樣,父執行緒就可以在worker完成任務後採取些必要的行動,比如更新DOM元素。如下程式碼所示:

primeWorker.addEventListener('message', function(event){
    console.log('Receiving from Worker: '+event.data);
    $('#prime').html( event.data );
});

  event物件包含兩個重要屬性:

  • target:用來指向傳送資訊的worker,在多元worker環境下比較有用。
  • data:由worker發回給父執行緒的資料。

  worker本身是包含在prime.js檔案中的,它同時偵聽message事件,從父執行緒中接收資訊。它同樣通過postMessage介面與父執行緒進行通訊。

self.addEventListener('message',  function(event){
    var currPrime = event.data, nextPrime;
    setInterval( function(){
    nextPrime = getNextPrime(currPrime);
    postMessage(nextPrime);
    currPrime = nextPrime;
    }, 500);
});

  在本文例子中,我們尋找下一個最大的質數,然後不斷將結果發回至父執行緒,同時不斷更新介面以顯示新的值。在worker的程式碼中,欄位self和this都是指向全域性作用域。Worker既可以新增事件偵聽器來偵聽message事件,也可以定義一個onmessage處理器,來接收從父執行緒發回的訊息。

  尋找下一個質數的例子顯然不是worker的理想用例,但是在此選用這個例子是為了說明訊息傳遞的原理。之後,我們會挖掘些可以通過web worker獲得益處的實際用例。

  終止Workers

  worker屬於佔用資源密集型,它們屬於系統層面的執行緒。因此,你應該不希望建立太多的worker執行緒,所以你需要在它完成任務後終止它。Worker可以通過如下方式由自己終止:

self.close();

  或者,由父執行緒終止。

primeWorker.terminate();

  安全與限制

  在worker的程式碼中,不要訪問一些重要的JavaScript物件,如document、window、console、parent,更重要的是不要訪問DOM物件。也許不用DOM元素以至不能更新頁面元素聽上去有點嚴格,但是這是一個重要的安全設計決定。

  想象一下,如果眾多執行緒都試著去更新同一個元素那就是個災難。所以,web worker需要處在一個嚴格的併執行緒安全的環境中。

  正如之前所說,你可以通過worker處理資料,並將結果返回主執行緒,進而更新DOM元素。儘管它們不能訪問一些重要的JavaScript物件,但是它們可以呼叫一些函式,如setTimeout()/clearTimeout()、setInterval()/clearInterval()、navigator等等,也可以訪問XMLHttpRequest和localStorge物件。

  同源限制

  為了能和伺服器互動,worker必須遵守同源策略(same-origin policy)(譯註:可參考國人文章同源策略)。比如,位於http://www.example.com/內的指令碼檔案不能訪問https://www.example.com的指令碼。儘管域名相同,但同源策略要求埠也必須一致。通常,這不會成為一個很大的問題。但是你很有可能會同一個域名編寫worker和client,所以知道這點對你總是有所幫助。

  Google Chrome與本地訪問

  Google Chrome對worker本地訪問做了限制,因此你無法本地執行這些例子。如果你又想用Chrome,那麼你可以將檔案放到伺服器上,或者在通過命令啟動Chrome時加上–allow-file-access-from-files。例如,蘋果系統下:

  $ /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome –allow-file-access-from-files

  然而,在實際產品生產過程中,此方法並不推薦。最好還是將你的檔案上傳至伺服器中,同時進行跨瀏覽器測試。

 Worker除錯和錯誤處理

  不能訪問console似乎有點不方便,但幸虧有了Chrome開發者工具,你可以像除錯其他JavaScript程式碼那樣除錯worker。

  為處理web worker丟擲的異常,你可以偵聽error事件,它屬於ErrorEvent物件。檢測該物件從中瞭解引起錯誤的詳細資訊。

primeWorker.addEventListener('error', function(error){
    console.log(' Error Caused by worker: '+error.filename
        + ' at line number: '+error.lineno
        + ' Detailed Message: '+error.message);
});

  多個Worker執行緒

  儘管建立多個worker來協調任務分配也許很常見,但還是要提醒一下各位,官方規範指出worker屬於相對重量級並能長期執行在後臺的指令碼。所以,由於Web worker的高啟動效能成本和高程式記憶體成本,它們的數量不宜過多。

  簡單介紹共享workers

  官方規範指出有兩種worker:專用執行緒(dedicated worker)和共享執行緒(shared worker)。到目前為止,我們只列舉了專用執行緒的例子。專用執行緒與建立執行緒的指令碼或頁面直接關聯,即有著一對一的聯絡。而共享執行緒允許執行緒在同源中的多個頁面間進行共享,例如:同源中所有頁面或指令碼可以與同一個共享執行緒通訊。

“建立一個共享執行緒,直接將指令碼的URL或worker的名字傳入SharedWorker建構函式”

  兩者最主要的區別在於,共享worker與埠相關聯,以保證父指令碼或頁面可以訪問。如下程式碼建立了一個共享worker,並宣告瞭一個回撥函式以偵聽worker發回的訊息 ,同時向共享worker傳輸一條訊息。

var sharedWorker = new SharedWorker('findPrime.js');
sharedWorker.port.onmessage = function(event){
    ...
}
sharedWorker.port.postMessage('data you want to send');

  同樣,worker可以偵聽connect事件,當有客戶端想與worker進行連線時會相應地向其傳送訊息。

onconnect = function(event) {
    // event.source包含對客戶端埠的引用
    var clientPort = event.source;
    // 偵聽該客戶端發來的訊息
    clientPort.onmessage = function(event) {
        // event.data包含客戶端發來的訊息
        var data = event.data;
        ....
        // 處理完成後發出訊息
        clientPort.postMessage('processed data');
    }
};

  由於它們具有共享的屬性,你可以保持一個應用程式在不同視窗內的相同狀態,並且不同視窗的頁面通過同一共享worker指令碼保持和報告狀態。想更多的瞭解共享worker,我建議你閱讀官方文件

  實際應用場景

  worker的實際發生場景可能是,你需要處理一個同步的第三方介面,於是主執行緒需要等待結果再進行下一步操作。這種情況下,你可以生成一個worker,由它代理,非同步完成此任務。

  Web worker在輪詢情況下也非常適用,你可以在後臺不斷查詢目標,並在有新資料時向主執行緒傳送訊息。

  你也許遇到需要向服務端返回大量的資料的情況。通常,處理大量資料會消極影響程式的響應能力,然後導致不良使用者體驗。更優雅的辦法是將處理工作分配給若干worker,由它們處理不重疊的資料。

  還有應用場景會出現在通過多個web worker分析音訊或視訊的來源,每個worker針對專項問題。

  結論

  隨著HTML5的展開,web worker規範也會持續加入。如果你打算使用web worker,看一看它的官方文件不是壞事。

  專項執行緒的跨瀏覽器支援目前還不錯,Chrome,Safari和Firefox目前的版本都支援,甚至IE這次都沒有落後太多,IE10還是不錯的。但是共享執行緒只有當前版本的Chrome和Safari支援。另外奇怪的一點是,Android 2.1的瀏覽器支援web worker,反而4.0版本不支援。蘋果也從iOS 5.0開始支援web worker。

  想象一下,在原本單執行緒環境下,多執行緒會帶來無限可能哦~

  《HTML5 web worker的使用

  《深入HTML5 Web Worker應用實踐:多執行緒程式設計

  英文原文:tutsplus,編譯:伯樂線上 - 胡蓉@蓉Flora

  文章連結:http://blog.jobbole.com/30445/

相關文章