multiple web workers的實現
一. 背景
先交代下業務背景,去年十月做了一個影片上傳的相關業務,部分需求如下:
影片檔案的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
XMLHttpReque
Array,Date,Math, and String
WindowTimers。setTimeout`and WindowTimers。setInterval
在一個worker中最主要的你不能做的事情就是直接影響父頁面。包括操作父頁面的節點以及使用頁面中的物件。你只能間接地實現,透過self。postMessage回傳訊息給主指令碼,然後從主指令碼那裡執行操作或變化。
特性:
1。為 JavaScript引入真正的執行緒,不必再使用 setTimeout()、setInterval()、XMLHttpRequest 來模擬並行
2。Worker 利用類似執行緒的訊息傳遞實現並行。這非常適合確保對 UI 的重新整理、效能以及對使用者的響應。
3。Web Worker 的三大主要特徵:能夠長時間執行(響應),理想的啟動效能以及理想的記憶體消耗。
適用場景
1。使用專用執行緒進行數學運算
Web Worker最簡單的應用就是用來做後臺計算,而這種計算並不會中斷前臺使用者的操作
2。影象處理
透過使用從
3。大量資料的檢索
當需要在呼叫 ajax後處理大量的資料,如果處理這些資料所需的時間長短非常重要,可以在Web Worker中來做這些,避免凍結
UI執行緒
。
4。背景資料分析
由於在使用Web Worker的時候,我們有更多潛在的CPU可用時間,我們現在可以考慮一下JavaScript中的新應用場景。
限制
1。不能訪問
DOM
和
BOM
物件(alert不支援,
console。log
部分瀏覽器支援,在safari中不能使用console,否則會報錯)
2。
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}
`) //——-> 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(`all : Fibonacci(${20}),Fibonacci(${10}):${res}
`) //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
序列化。所以有一點雞肋的地方就是我們需要把整段
業務程式碼
全寫在一個方法裡。
參考文獻: