multiple web workers的實現
來自專欄新浪移動大前端27 人贊了文章
一. 背景
先交代下業務背景,去年十月做了一個視頻上傳的相關業務,部分需求如下:
- 視頻文件的MD5計算
- 並行上傳,可配置最大並行數。
- 分片上傳
- 可隨時中斷,取消上傳。
以上只是上傳部分的功能,對於我這種第一次做上傳的人來說,看了真是一頭霧水。不僅要解決上述的需求,還要考慮其他的設計和性能問題,比如:
- js單線程:當上傳一個5G+的大文件,計算MD5的時間約幾分鐘,在主線程中無法同時計算多個視頻的md5,此後添加的文件都在隊列中,需要一個一個計算MD5。
- 並行上傳:js主線程基於eventloop,無法做到真正意義上的並行上傳。
- 分片上傳:切片視頻,頻繁的讀寫視頻文件。
- 維護上傳隊列:當文件上傳完成或者取消時,自動添加上傳文件。
對於這種需求,webworker 是最合是不過的了。
所以當時按照如下方式分解了業務功能,解決了上述問腿。
- js的主線程負責創建web worker,相關UI視圖,更新UI。
- worker 負責 文件計算MD5,切片,上傳,計算相關數據。
- 處理文件,上傳時 如需更新UI,worker將相關數據傳遞給主線程,主線程更新相關UI視圖。
- 主線程需要對文件 ,上傳 進行計算 和 處理時,通知worker,worker完成相關操作
基於以上,覺得可以把worker定義為一個執行複雜運算的線程,將想要執行的方法通過postmessage 方法 傳遞給 worker,當worker接收到後開始執行,並將結果返回給主線程。所以寫了個multi-worker。
二.什麼是web worker
在 HTML5 中,工作線程的出現使得在 Web 頁面中進行多線程編程成為可能。眾所周知,傳統頁面中(HTML5 之前)的 JavaScript 的運行都是以單線程的方式工作的,雖然有多種方式實現了對多線程的模擬(例如:JavaScript 中的 setinterval 方法,setTimeout 方法等),但是在本質上程序的運行仍然是由 JavaScript 引擎以單線程調度的方式進行的。在 HTML5 中引入的工作線程使得瀏覽器端的 JavaScript 引擎可以並發地執行 JavaScript 代碼,從而實現了對瀏覽器端多線程編程的良好支持。
HTML5 中的 Web Worker 可以分為兩種不同線程類型,一個是專用線程 Dedicated Worker,一個是共享線程 Shared Worker。兩種類型的線程各有不同的用途
web-worker兼容性:
worker中可用的函數和介面
你可以在web worker中使用大多數的標準javascript特性,包括
你可以在web worker中使用大多數的標準javascript特性,包括
Navigator
XMLHttpRequeArray,Date,Math, and StringWindowTimers.setTimeout`and WindowTimers.setInterval
在一個worker中最主要的你不能做的事情就是直接影響父頁面。包括操作父頁面的節點以及使用頁面中的對象。你只能間接地實現,通過self.postMessage回傳消息給主腳本,然後從主腳本那裡執行操作或變化。
特性:
1.為 JavaScript引入真正的線程,不必再使用 setTimeout()、setInterval()、XMLHttpRequest 來模擬並行
2.Worker 利用類似線程的消息傳遞實現並行。這非常適合確保對 UI 的刷新、性能以及對用戶的響應。
3.Web Worker 的三大主要特徵:能夠長時間運行(響應),理想的啟動性能以及理想的內存消耗。
適用場景
1.使用專用線程進行數學運算
Web Worker最簡單的應用就是用來做後台計算,而這種計算並不會中斷前台用戶的操作2.圖像處理通過使用從<canvas>或者<video>元素中獲取的數據,可以把圖像分割成幾個不同的區域並且把它們推送給並行的不同Workers來做計算3.大量數據的檢索當需要在調用 ajax後處理大量的數據,如果處理這些數據所需的時間長短非常重要,可以在Web Worker中來做這些,避免凍結UI線程。4.背景數據分析由於在使用Web Worker的時候,我們有更多潛在的CPU可用時間,我們現在可以考慮一下JavaScript中的新應用場景。
限制
1.不能訪問
2.DOM
和BOM
對象(alert不支持,console.log部分瀏覽器支持,在safari中不能使用console,否則會報錯)Location
和navigator
的只讀訪問,並且navigator
封裝成WorkerNavigator
對象,有部分屬性被更改。3.無法讀取本地文
4.全局變數中不存在this
,this
並不指向window
。有self
,指向worker
本身5.子線程和父級線程的通訊是通過值拷貝,子線程對通信內容的修改,不會影響到主線程。在通訊過程中值過大也會影響到性能(解決這個問題可以用transferable objects
)6.條數限制,大多瀏覽器能創建web worker
線程的條數是有限制的,可以手動去拓展,但是如果不設置的話,基本上都在20條以內,每條線程大概5M左右,需要手動關掉一些不用的線程才能夠創建新的線程(相關解決方案)
通信方法:
- 發送消息
主線程 :worker.postMessage();
worker線程 :self.postMessage();
- 接收消息
主線程:worker.message();
worker線程:self.message();
- 監聽異常
主線程:worker.error();
worker線程:self.error();
- 銷毀方法
主線程:worker.terminate();
worker線程:self.close();
三.API設計
背景需求里要求實現隊列,所以在multi-worker 里增加了隊列控制,可以在創建multi-worker實例時配置最大並行執行的worker數量,默認是window.navigator.hardwareConcurrency。
config: { maxWorkers : (window.navigator && window.navigator.hardwareConcurrency) || 3, minWorkers : 1}
對於一個worker的維護隊列主要提供增,刪,查三種方法就夠了,每個worker都會分配一個id,方便我們操作指定worker。每個方法都會返回worker的實例。
add(config = { //增 id:id, fn:fn, args:args,}) { return new worker(config);}getWorker(id) // 查getIdleWorker(id) //查removeWorker(id) // 刪
此外,還提供race
/ all
方法: 返回最先 / 全部 在worker中執行完成的結果。因為postmessage本身是個一來一回的非同步的行為,包裝成promise的肯定更為合適和易用。
all/race(excuFns){ let racePWorkers = []; let promises = []; excuFns.forEach((excuFn) => { let worker = this.add(); racePWorkers.push(worker); promises.push(worker.reslover.promise); }) racePWorkers.forEach((worker, index) => { worker.excu(excuFns[index].fn, excuFns[index].args); }); return Promise.race(promises)}
worker 方法:
worker只提供一個excu(fn,args)方法,用於執行指定的函數方法。返回一個promise,非同步接收worker中執行的結果。
excu(fn,args){ if(this.busy)throw new Error (`id:${this.id} worker is busy`); let _fn , _args; if (fn && typeof fn === function) { _fn = GeneralUtils.serializeFunction(fn ); _args = GeneralUtils.stringifyJson(args); } this.worker.postMessage({ _fn, _args}) this.busy = true; return this.reslover.promise; }
下面我們看一下,具體的使用方法:
const multiWorker = new mWorker({ maxWorkers: 1});function recurFib(n) { if (n == 1 || n == 2) { return 1; } return recurFib(n - 1) + recurFib(n - 2);}// 創建指定id的worker,計算multiWorker.add({id:10}).excu(recurFib,[10]).then((res)=>{ console.log(創建指定id worker,計算); console.log(res); document.write(`Fibonacci(${10}):${res}<br>`) //---> output 55})//all 方法var allWorker = multiWorker.all([{ fn: recurFib, args: 20 }, { fn: recurFib, args: 10 }]);allWorker.then((res) => { console.log(all方法, res);// [6765,55]; document.write(`<mark>all</mark> : Fibonacci(${20}),Fibonacci(${10}):<mark>${res}</mark><br>`) //6765,55}).then(()=>{ //終止 全部worker setTimeout(() => { multiWorker.removeWorker(); document.write(`全部worker已銷毀`); }, 2000)})
四.實現原理
實現其實蠻簡單的。
- web-worker受同源策略的限制,Worker 不能讀取本地文件,所以這個腳本必須來自網路。通過worker-loader在編譯打包時,把本地worker文件處理。
- worker線程如何處理主線程傳來的方法:主線程把需要在worker中執行的方法通過postmessage傳給worker,worker接收到後,通過eval執行此方法,運行結束後,得到結果再通過postmessage傳遞給主線程。
web-worker 中 用eval執行主線程傳遞的方法eval(( + _fn + ));
五.優化
Worker 與「主線程」之間的數據傳遞默認是通過結構化克隆(Structured Clone)。但數據量較大時,克隆過程會比較耗時,會影響 postMessage 和 onmessage 函數的執行時間。可以先通過 JSON.stringify
將對象序列化,接收之後再用 JSON.parse
還原。國外大神測試使用JSON.stringify
和 JSON.parse
的性能對比。 測試版本有點低,不過能說明使用stringify
的性能更好一些。
(還有一種避開克隆傳值的方法,就是使用Transferable Objects,主要是採用二進位的存儲方式,採用地址引用,解決數據交換的實時性問題;Transferable Objects支持的常用數據類型有ArrayBuffer,ImageBitmap)
六.總結和問題
寫完發現有幾個類似的庫,實現原理差不多。multi-worker的有點是promise化,增加了隊列和race/all方法。
但是有兩個問題,如果有什麼解決辦法歡迎提意見。
1.由於webworker也是基於eventloop,所以,在worker中執行方法時,無法通過主線程的worker.terminate()終止此worker,僅能等待當前任務執行完畢後才能終止。
2.通postmessage傳進來的函數,無法引用此函數以外的函數,因為在postmessage前,會通過Json.stringify
序列化。所以有一點雞肋的地方就是我們需要把整段業務代碼全寫在一個方法里。
參考文獻:
Web WorkersHigh-performance Web Worker messages深入 HTML5 Web Worker 應用實踐:多線程編程推薦閱讀:
※人工智慧/comp9414 Chapter1-2
※你見過的最難的編程語言是什麼?
※Linux應用開發自學之路
※為什麼Haskell不需要Macro
※如何批量打開或關閉onenote筆記本?