JavaScript是單線程的,又是異步的,而最新的HTML5中,通過Web Workers可以在JS中支持多線程開發。這是幾個意思?異步還是單線程,這怎么理解?Web Workers又是什么原理?實際開發中,異步和多線程之間如何交互?答案就在下面。主要涉及的內容有:
-
為什么異步解決不了問題
-
Worker又是什么玩法
-
Cesium中的異步+多線程框架
為什么異步解決不了問題
簡單說,JavaScript是單線程的,簡單易用,但如果遇到時間較長的任務時,則容易出現卡死的現象,為了避免這種問題,我們對時間久的任務采用異步的方式,保證頁面的快速響應。
比如我們常見的setTimeout,指定某個時間運行,然后在指定時間運行該函數。然而“JS運行在單線程環境中,定時器僅僅是計划代碼在未來某個時間執行,並不作為保證執行時間,因為不同時間可能有其他代碼在控制JS進程,而所有函數必須使用相同的線程執行。實際上,由瀏覽器負責排序,指派某段代碼在某個時間點運行的優先級”。在這里,單線程,異步又該如何理解?這就需要我們了解一下異步的原理。
摘自《Secrets of theJavaScript Ninja》
這個圖初看有點晦澀,沉下心來好好看一遍,然后在看看這段文字解釋,相信你會大有收獲。首先,右側是JS引擎所觸發的代碼,左側是事件隊列,0,10,20則是自上而下的時間軸,我們就以毫秒為單位吧。
首先,在2ms處,執行了setTimeout語句,設定10ms后執行fun1函數;在5ms處出現了鼠標點擊事件,執行fun2函數;接着在10ms處出執行了setInterval,設定10ms后執行fun3函數。而整個JS代碼塊執行大約用了18ms。因此,首先當鼠標點擊后的回調時間fun2以及setTimeout所觸發的fun1函數發現,此時JS代碼塊還控制着執行進行,則兩者都進入隊列,等待一個合適的時機在運行
這時,在18ms處,JS代碼塊終於運行完了,機會來了,這時鼠標的callback回調關聯着一個異步事件(因為我們無法知道用戶想要何時點擊鼠標,所以我們認為回調事件是異步的),所以很不幸,fun1事件還是要繼續呆在隊列中。同時,在20ms出,觸發了第一次setInterval,當然一視同仁,所以fun3也進入隊列。
28ms處,終於鼠標回調事件結束了,看看隊列里面,setTimeout的fun1函數終於有了出頭日,開始執行fun1函數,隊列中僅剩下setInterval的fun3函數。在30ms時,setInterval又調用了一次,但發現隊列中上一次的函數還未運行,所以這一次的觸發沒有任何效果,丟棄掉。
終於36ms后,Time觸發的fun1運行完畢,隊列中僅剩的fun3函數開始運行,在40ms時,setInterval再次周期觸發,但此時js進程還是由fun3函數控制,所以觸發事件進入隊列。
以此類推,一直運行到隊列為空時,這樣一旦有事件觸發,則會直接運行。 希望所有人能認真理解這個過程,並發現setTimeout和setInterval在處理上的相同和不同處,這塊不是本文重點,所以不多討論。
通過這樣一個過程,相信大家理解了異步和單線程之間的關系:JS在一個線程中運行,但通過消息隊列來實現異步調用,但調用本身也是在同一個線程中運行,只是可以延后或分解任務。舉個不太妥當的例子:假如只有一個出租車司機,相當於JS的進程,模擬一個線程的情況,而乘客相當於異步請求,通過滴滴打車,可以約定某個時間來接你,然后到達目的地(函數實現)。但觸發並不等同於運行,乘客下單時,司機還在載其他客人,但答應在約定時間接你。這時他載完該乘客后立馬去接你,滿足你的請求。而在此之前,各自忙各自的,他在執行他的任務,你有可能在等,或者在刷手機(服務端接收請求,並返回結果)。
異步確實能盡可能的優化,比如Ajax等異步請求。但這要求把任務分解的比較簡單,在時間比較久的任務下還是會出現無響應的問題,不管你的進度條做的有多好看。
Worker又能干什么事情
異步只是看上去更及時而已,但該花的時間一點也不會少,而且因為調度本身的成本,時間還會多花一點。而且,隨着Web應用的不斷發展 ,在JS端要求的計算量也越來越大,這種時候,Web Worker可以讓JS在后台解決這些問題,而不必擔心影響用戶體驗。
需要注意的是,Worker線程完全在另一個作用域中,而且無法操作DOM元素,不能與網頁代碼共享作用域。但這已經足夠了,比如排序,或者zip壓縮等操作,都可以放到Worker線程來運行,從而能夠在Web端進行類似CS的很多應用。
Worker的具體使用這里也不介紹,主要解釋一下下面這張圖:
摘自AlloyTeam團隊《深入理解Web Worker》
main.js中,在創建woker線程后,立即調用了postMessage方法傳遞了數據,在worker線程還沒創建完成時,main.js中發出的消息,會先存儲在一個臨時消息隊列中,當異步創建worker線程完成,臨時消息隊列中的消息數據復制到woker對應的WorkerRunLoop的消息隊列中,worker線程開始處理消息。在經過一輪消息來回后,繼續通信時, 這個時候因為worker線程已經創建,所以消息會直接添加到WorkerRunLoop的消息隊列中 ---摘自AlloyTeam團隊《深入理解Web Worker》
這是Worker線程和主線程的一個交互方式,首先可見消息的發送和接收采用的是postmessage和onmessage,相信做過MFC開發的一看也能發現,這也是一個異步消息隊列的傳輸方式。
在數據傳輸中,或許在Worker線程中采用同步,效果會更好。另外,在參數的傳遞是拷貝方式,但同時提供Transferable Objects方式,可以傳地址(不是拷貝)並加鎖,這是一個非常實用的參數,特別是在比較大的二進制數值運算中。
如果需要在worker腳本中加載其他js文件,則使用importScripts函數,這是一個同步過程,所以性能會有影響,不過既然是在工作者線程中,所以也不太嚴重。
還有一個問題,在產品化的時候如何混淆壓縮這些worker.js腳本,因為我們需要引入它們,所以造成了這部分代碼很容易format,讓別人下載分析。雖然技術在於分享,畢竟作為產品,這也是需要考慮的部分,總不能直接源碼提供吧。我看到Google WebGL Earth上有一個方式,采用Blob的思路內嵌Worker。因為我還沒用過,這里也不多說了,只提供這樣一個思路。
Cesium中的異步+多線程框架
說了這么多,下面和大家分享一下Cesium中多線程設計的框架吧,我覺得很專業,但也有些復雜,但復雜的同時帶來了很好的擴展性。簡單來說就是一個插件的思路。
Cesium中設計到三維球的很多計算,數據量很大,比如地形的三角網,以及參數化的Geometry中vbo的計算,而這些都是在Worker中實現的,參數的傳遞,不同類型之間的算法也不同,所以設計一個易用且易擴展的Worker框架則顯得非常有必要。
如上圖,用戶只需要創建一個TaskProcessor,指定具體需要創建線程的類型,比如(圓,面,還是線),然后調用scheduleTask,里面是該對象的具體參數,比如圓就是圓心+半徑,這樣便完成了調用過程。那返回結果怎么接受呢?大家注意最后一行返回的參數Promise,這也是一個Promise的異步方式,用戶自然能夠方便的獲取到結果。下面是返回結果的實現。
當然使用的簡單,多數意味着實現的復雜。這里主要和大家說一下用戶指定Worker的名字,如果根據名字創建該Worker線程,並且易於擴展,也就是插件的實現思路。
首先,有一個cesiumWorkerBootstrapper的Worker,所有createWorker都會建立一個cesiumWorkerBootstrapper線程,只是賦予不同的參數(name不同)。
而在cesiumWorkerBootstrapper線程中,使用了requirejs,根據指定的路徑和文件名,獲取對應的函數,同時替換的onmessage函數。
此時,主線程在調用scheduleTask時,會再次發送postmessage,並傳入參數,而此時requirejs已經找到了對應的功能函數。,即替換onmessage的函數。
而這些函數都是由createTaskProcessorWorker封裝的匿名函數,類似於回調函數,進而實現對應的功能。並且返回指定結果。
這樣,一個多線程設計框架就完成了,並且通過Promise機制,方便用戶的使用,而內部使用require.js,實現了插件的這樣一個方式。這塊代碼涉及的內容比較多,這里也是理解思路,具體的細節還是需要代碼的調試才能更好的理解,這里也僅僅提供參考。








