本文首發在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