【轉向Javascript系列】深入理解Web Worker


本文首發在alloyteam團隊博客,鏈接地址http://www.alloyteam.com/2015/11/deep-in-web-worker/

上一篇文章《從setTimeout說事件循環模型》從setTimeout入手,探討了Javascript的事件循環模型。有別於Java/C#等編程語言,Javascript運行在一個單線程環境中,對setTimeout/setInterval、ajax和dom事件的異步處理是依賴事件循環實現的。作為一個轉向Javascript的開發人員,很自然的產生一個疑問,如何實現Javascript多線程編程呢?隨着學習的深入,我了解到HTML5 Web Worker,本文將分析Web Worker為Javascript帶來了什么,同時帶大家看看worker模型在其他語言的應用。

 

1.Web Worker是什么

Web Worker 是HTML5標准的一部分,這一規范定義了一套 API,它允許一段JavaScript程序運行在主線程之外的另外一個線程中。Web Worker 規范中定義了兩類工作線程,分別是專用線程Dedicated Worker和共享線程 Shared Worker,其中,Dedicated Worker只能為一個頁面所使用,而Shared Worker則可以被多個頁面所共享,本文示例為專用線程Dedicated Worker。

 

1.1 API快速上手

使用Dedicated Worker的主頁面代碼main.js

var worker = new Worker("task.js");
worker.postMessage(
        {
            id:1,
            msg:'Hello World'
        }
);
worker.onmessage=function(message){
    var data = message.data;
    console.log(JSON.stringify(data));
    worker.terminate();
};
worker.onerror=function(error){
    console.log(error.filename,error.lineno,error.message);
}

Dedicated Worker所執行的代碼task.js

onmessage = function(message){
    var data=message.data;
    data.msg = 'Hi from task.js';
    postMessage(data);
}

在main.js代碼中,首先通過調用構造函數,傳入了worker腳本文件名,新建了一個worker對象,在我的理解中,這一對象是新創建的工作線程在主線程的引用。隨后調用worker.postMessage()方法,與新創建的工作線程通信,這里傳入了一個json對象。隨后分別定義了worker對象的onmessage事件和onerror事件的回調處理函數,當woker線程返回數據時,onmessage回調函數執行,數據封裝在message參數的data屬性中,調用 worker 的 terminate()方法可以終止worker線程的運行;當worker線程執行出錯時,onerror回調函數執行,error參數中封裝了錯誤對象的文件名、出錯行號和具體錯誤信息。

 

在task.js代碼中,定義了onmessage事件處理函數,由主線程傳入的數據,封裝在message對象的data屬性中,數據處理完成后,通過postMessage方法完成與主線程通信。在工作線程代碼中,onmessage事件和postMessage方法在其全局作用域可以訪問。

 

1.2  worker線程執行流程

通過查閱資料,webKit加載並執行worker線程的流程如下圖所示

1)     worker線程的創建的是異步的

代碼執行到"var worker = new Worker(task.js')“時,在內核中構造WebCore::JSWorker對象(JSBbindings層)以及對應的WebCore::Worker對象(WebCore模塊),根據初始化的url地址"task.js"發起異步加載的流程;主線程代碼不會阻塞在這里等待worker線程去加載、執行指定的腳本文件,而是會立即向下繼續執行后面代碼。

 

2)     postMessage消息交互由內核調度

main.js中,在創建woker線程后,立即調用了postMessage方法傳遞了數據,在worker線程還沒創建完成時,main.js中發出的消息,會先存儲在一個臨時消息隊列中,當異步創建worker線程完成,臨時消息隊列中的消息數據復制到woker對應的WorkerRunLoop的消息隊列中,worker線程開始處理消息。在經過一輪消息來回后,繼續通信時, 這個時候因為worker線程已經創建,所以消息會直接添加到WorkerRunLoop的消息隊列中;

 

1.3 worker線程數據通訊方式

主線程與子線程數據通信方式有多種,通信內容,可以是文本,也可以是對象。需要注意的是,這種通信是拷貝關系,即是傳值而不是地址,子線程對通信內容的修改,不會影響到主線程。事實上,瀏覽器內部的運行機制是,先將通信內容串行化,然后把串行化后的字符串發給子線程,后者再將它還原。

 

主線程與子線程之間也可以交換二進制數據,比如File、Blob、ArrayBuffer等對象,也可以在線程之間發送。但是,用拷貝方式發送二進制數據,會造成性能問題。比如,主線程向子線程發送一個50MB文件,默認情況下瀏覽器會生成一個原文件的拷貝。為了解決這個問題,JavaScript允許主線程把二進制數據直接轉移給子線程,轉移后主線程無法再使用這些數據,這是為了防止出現多個線程同時修改數據的問題,這種轉移數據的方法,叫做Transferable Objects。

// Create a 32MB "file" and fill it.
var uInt8Array = new Uint8Array(1024*1024*32); // 32MB
for (var i = 0; i < uInt8Array .length; ++i) {
    uInt8Array[i] = i;
}
worker.postMessage(uInt8Array.buffer, [uInt8Array.buffer]);

 

1.4  API進階

在worker線程中,可以獲得下列對象

1)     navigator對象

2)     location對象,只讀

3)     XMLHttpRequest對象

4)     setTimeout/setInterval方法

5)     Application Cache

6)     通過importScripts()方法加載其他腳本

7)     創建新的Web Worker

worker線程不能獲得下列對象

1)     DOM對象

2)     window對象

3)     document對象

4)     parent對象

上述的規范,限制了在worker線程中獲得主線程頁面相關對象的能力,所以在worker線程中,不能進行dom元素的更新。

 

2. 似曾相識worker模型

我在學習Web Worker過程中,總有一種似曾相似的感覺。在以往的學習經驗中,了解過Java Swing GUI庫中的Swing Worker,我們可以看看worker模型在Swing中的應用。

 

2.1 Swing事件分發模型

同Winform/WPF等其他GUI庫一樣,Swing是一個基於事件隊列的單線程編程模型。Swing將GUI請求放入一個事件隊列EventQueue 中等待執行,EventQueue的派發機制由單獨的一個線程管理,這個線程稱為事件派發線程(EventDispatchThread),負責GUI組件的繪制和更新。這一事件分發模型如下圖所示:

Swing單線程模型的一個問題是,如果在“事件派發線程”上執行的運算太多,那么GUI界面就會停住,系統響應和運算就會非常緩慢。

 

既然事件派發線程是為了處理GUI事件而設的,那么,我們只應該把GUI事件處理相關的代碼,放在事件派發線程中執行。其他與界面無關的代碼,應該放在Java其他的線程中執行。這樣,我們在Swing的事件處理中,仍然使用Swing的單線程編程模型,而其他業務操作均使用多線程編程模型,這就可以大大提高Swing程序的響應和運行速度,充分運用Java多線程編程的優勢。

 

2.2 Swing Worker

Java SE 6提供了javax.swing.SwingWorker類,Swing Worker 設計用於需要在后台線程中運行長時間運行任務的情況,並可在完成后或者在處理過程中向 UI 提供更新。

假定我們在UI界面點擊一次下載按鈕,在按鈕的事件處理函數中,需要去加載一張Icon圖片,圖片加載完成后,將icon在UI界面展示出來。

SwingWorker testWorker = new SwingWorker<Icon , Void>(){  
      @Override  
       protected Icon doInBackground() throws Exception {  
        Icon icon = retrieveImage(strImageUrl);   
            return icon;   
       }   
  
       protected void done(){   
            Icon icon= get();  
            lblImage.setIcon(icon); //lblImage可通過構造函數傳入  
      }             
}  
testWorker.execute();

上述代碼中,我們在按鈕的事件處理函數中,創建了一個swingworker實例對象。調用構造函數時,指定第一個泛型參數為Icon,這是一個自定義類型,這里代表一個Icon圖片對象。指定這一泛型參數,是為了指定doInBackground()方法的返回值,並在done()方法中獲取。

 

doInBackground方法作為工作線程的一部分執行,它負責完成線程的基本任務,並以返回值來作為線程的執行結果。在doInBackground方法完成之后,SwingWorker調用done方法。如果任務需要在完成后,使用工作線程執行結果來更新GUI組件或者做些清理工作,可覆蓋done方法來完成它們。使用SwingWorker的get方法可以獲取doInBackground方法的結果,done方法是調用get方法的最好地方,因為此時已知道線程任務完成了,SwingWorker在EDT上激活done方法,因此可以在此方法內安全地和任何GUI組件交互。execute方法是異步執行,它立即返回到調用者。在execute方法執行后,EDT立即繼續執行。

 

2.3 WebWorker vs SwingWorker

Swing Worker還有一些其他的方法,這里不再討論。我們可以結合Web Worker,對比看看兩者異同。

兩者編程模型相同,都是在主線程中,將耗時工作交由工作線程去異步的完成,從而避免主線程的阻塞。

 

兩者線程通信機制不同,Web Worker線程通信限制嚴格,僅能通過postMessage方法通信,而且參數傳遞均為值傳遞,沒有引用傳遞;Swing Worker參數傳遞靈活,上述事例中,testWorker的doInBackground方法直接引用了strImageUrl變量,不過這一方式並不推薦,而是應當定義一個新類繼承自SwingWorker,並在構造函數中傳入imgUrl變量,然后在實例化worker線程中傳入變量。

 

兩者對UI界面的更新限制不同,Web Worker禁止在worker線程中操作dom元素,所以不能在worker中更新UI;Swing Worker允許在done方法中更新UI,這里並沒有違背Swing的事件分發模型,因為最終還是在EDT上激活的done方法,依然遵循着事件分發模型。

 

3. Web Worker帶來了什么

最后來總結Web Worker為javascript帶來了什么,學習過程中,看到一些文章認為Web Worker為Javascript帶來了多線程編程能力,我不認可這種觀點。

3.1 Web Worker帶來后台計算能力

Web Worker自身是由webkit多線程實現,但它並沒有為Javasctipt語言帶來多線程編程特性,我們現在仍然不能在Javascript代碼中創建並管理一個線程,或者主動控制線程間的同步與鎖等特性。

在我看來,Web Worker是worker編程模型在瀏覽器端Javascript語言中的應用。瀏覽器的運行時,同其他GUI程序類似,核心邏輯像是下面這個無限循環: 

while(true){  
    1 更新數據和對象狀態  
    2 渲染可視化UI  
}

在Web Worker之前,Javascript執行引擎只能在一個單線程環境中完成這兩項任務。而在其他典型GUI框架,如前文Swing庫中,早已引入了Swing Worker來解決大量計算對UI渲染的阻塞問題。Web Worker的引入,是借鑒了worker編程模型,給單線程的Javascript帶來了后台計算的能力。

3.2 Web Worker典型應用場景

既然Web Worker為瀏覽器端Javascript帶來了后台計算能力,我們便可利用這一能力,將無限循環中第一項“更新數據和對象狀態”的耗時部分交由Web Worker執行,提升頁面性能。

部分典型的應用場景如下

1)  使用專用線程進行數學運算

Web Worker最簡單的應用就是用來做后台計算,而這種計算並不會中斷前台用戶的操作

2)  圖像處理

通過使用從<canvas>或者<video>元素中獲取的數據,可以把圖像分割成幾個不同的區域並且把它們推送給並行的不同Workers來做計算

3)  大量數據的檢索

當需要在調用 ajax后處理大量的數據,如果處理這些數據所需的時間長短非常重要,可以在Web Worker中來做這些,避免凍結UI線程。

4)  背景數據分析

由於在使用Web Worker的時候,我們有更多潛在的CPU可用時間,我們現在可以考慮一下JavaScript中的新應用場景。例如,我們可以想像在不影響UI體驗的情況下實時處理用戶輸入。利用這樣一種可能,我們可以想像一個像Word(Office Web Apps 套裝)一樣的應用:當用戶打字時后台在詞典中進行查找,幫助用戶自動糾錯等等。

 

參考文章

1.The Basics of Web Workers

http://www.html5rocks.com/en/tutorials/workers/basics/

2. 深入 HTML5 Web Worker 應用實踐:多線程編程

http://www.ibm.com/developerworks/cn/web/1112_sunch_webworker/index.html

3. JavaScript 工作線程實現方式

http://www.ibm.com/developerworks/cn/web/1105_chengfu_jsworker/index.html

4.HTML5 與 ”性工能“障礙

http://fins.iteye.com/blog/1747321

5.Web Worker在WebKit中的實現機制

http://blog.csdn.net/codigger/article/details/40581343

6. SwingWorker的用法

http://blog.csdn.net/vking_wang/article/details/8994882


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM