有關深入理解JavaScript中的並行處理的介紹

才智咖 人氣:3.18W

前言

有關深入理解JavaScript中的並行處理的介紹

為什麼說多執行緒如此重要?這是個值得思考的問題。一直以來,派生執行緒以一種優雅的方式實現了對同一個程序中任務的劃分。作業系統負責分配每個執行緒的時間片,具有高優先順序並且任務繁重的執行緒將分配到更多的時間片,而低優先順序空閒的執行緒只能分到較少的時間片。

雖然多執行緒如此重要,但JavaScript卻並沒有多執行緒的能力。幸運的是,隨著 Web Worker 的普及,我們終於可以在後臺執行緒來處理資源密集型的計算了。而不好的方面是,目前制定的標準只適用於當前的生態系統,這有時候就比較尷尬了。如果你瞭解其他從一開始就支援多執行緒的語言的話,你可能會發現很多的限制,遠非僅僅是例項化一個新執行緒,然後你操控這個例項就能實現多執行緒。

這篇文章主要來介紹 Web Worker,包括什麼時候使用,該怎麼使用,它有什麼奇怪的特性,會介紹在 Webpack 中如何使用它,還有可能遇到的'一些坑。

一、Web Workers

Web Worker 可能是在 JavaScript 中唯一可以真正實現多執行緒的方法了。我們需要按照下面的方式建立 worker :

const worker = newWorker("worker.js");

上面就定義了一個 Worker 例項,然後你可以通過 postMessage 與 worker 通訊,就像和 iFrame 通訊一樣,只不過不存在跨域的問題,不需要驗證跨域。

worker.postMessage(num);

在 worker 程式碼中,你需要監聽這些事件:

onmessage = (e) => { // e.data will contain the value passed};

這種方式是雙向的,所以你也可以從 worker 中 postMessage 給我們的主程式。

在 worker 程式碼中:

postMessage(result);

在主程式中:

worker.onmessage = (e) => {}

這就是 worker 最基本的用法。

異常處理

在你的 worker 程式碼中,有很多種方式來處理異常,比如你可以 catch 之後通過 postMessage 傳遞,這樣可能需要多些一些程式碼,但是確實最有效也最安全的。

另一種方式是用 onerror 事件,這種方式可以捕捉所有未處理的異常,並且交給呼叫方來決定如何處理。呼叫方式很簡單:

worker.onerror = (e) => {};

為了除錯方便,異常物件中還有一些額外的欄位比如:filename,lineno,colno.

回收

將不需要的 worker 回收是非常重要的,worker 會生成真正的作業系統執行緒,如果你發現與很多 worker 執行緒同時執行,你可以通過很簡單的殺掉瀏覽器程序。

你有兩種方式殺掉 worker 程序:在 worker 裡和在 worker 外。我認為最好的處理 worker 生命週期的地方是在主頁面裡,但這也要取決於你程式碼的具體情況。

殺掉一個 worker 例項,在外部可以直接呼叫 terminate()方法,這種方法可以立即殺掉它,釋放所有它正在使用的資源,如果它正在執行,也會立即終止。

如果你想要讓 worker 自己去管理它的生命週期,可以直接在 worker 程式碼中呼叫stop()方法。

不管使用哪種方法,worker 都會停止,銷燬所有資源。

如果你想使用一種“一次性”的 worker,比如需要做一些複雜運算之後就不再使用了,也要確保在 onerror 事件中去銷燬它們,這樣有利於規避一些難以發現的問題。

worker.onerror = (e) => { worker.terminate(); reject(e);};worker.onmessage = (e) => { worker.terminate(); resolve(e.data);}

二、行內 Workers

有些時候將 worker 程式碼寫到一個外部檔案可能會使原本簡單的問題變得複雜,幸運的是,workers 也可以用一個 Blob 來初始化。

寫一個行內 worker ,參考如下程式碼段:

// Put your worker code here

const code = URL.createObjectURL(new Blob([ document.getElementById("worker").textContent]));const worker = new Worker(code);

這樣你就建立了一個全域性的 ObjectURL,但別忘了當不需要的時候要銷燬它:

worker.terminate();URL.revokeObjectURL(code);

三、Workers 巢狀

理論上,你可以巢狀使用 worker,就像在主執行緒中定義一個 worker 一樣。這裡有一個簡單的 例子。但是不幸的是在 Chrome 中一直存在一個 bug ,讓我們不能愉快的玩耍,或許以後這個 bug 會修復,但是目前來說還是沒有太多進展,所以你最好不要使用。

資料傳遞

在 worker 資料傳遞的過程中有些需要注意的邊緣情況。你可以傳遞數值,字串,陣列,也可以傳遞序列化/反序列化的物件。然而,你卻不應該依賴序列化來保持資料結構,實際上,postMessage 用到了一種 資料克隆演算法,它會生成一些額外的屬性比如 RegExps 和 Blobs 以及一些迴圈引用。

這就是說,你需要將你要傳遞的資料最小化。你不可以傳遞 functions ,即使是支援的型別也會有一些限制,這些也很容易產生一些難以發現的 bug。如果你將你的 API 定義為只傳遞字串,數值,陣列和物件的話,那你可能會避過這些問題。

迴圈引用

如果你有一個很複雜的物件,那麼裡面很可能存在迴圈引用,這時如果你將它序列化成 JSON,你將會得到一個 TypeError: Converting circular structure to JSON.

let a = {};let b = {a};a.b = b;JSON.stringify({a,b}); // Error

然而你可以在 postMessage 中放心的使用,從而你就可以在 worker 中使用。

Transferable objects

為了防止同時修改同一變數的場景,你傳遞給 postMessage 的所有變數都會複製一份,這樣確保了你多個執行緒不會修改同一個變數。但如果你想要傳一個非常大的資料的話,你就會發現複製操作是很慢的。比如,如果你在做一些圖片相關的運算,你可能會傳遞整個圖片資訊,就可能會遇到複製效能的瓶頸。

好在有 transferable object ,用 transfer 來代替 copy,比如ArrayBuffer 是transferable物件,而我們可以把任何型別的物件放在 ArrayBuffer 中。

如果你 transfer 一個物件,之前擁有它的執行緒被鎖定許可權,它確保了資料沒有複製之前,不會被同時修改。

這時 postMessage 的程式碼段就有點尷尬了:

const ab = new ArrayBuffer(100);console.log(ab.byteLength); // 100worker.postMessage(ab, [ab]);console.log(ab.byteLength); // 0

確保在 postMessage 中傳遞第二個引數,否則資料將會被複制。

const ab = new ArrayBuffer(100);console.log(ab.byteLength); // 100worker.postMessage(ab);console.log(ab.byteLength); // 100

四、Webpack

在 Webpack 中使用 Web worker 時,你需要用 worker-loader。將它新增到 package.json 中的 devDependencies,然後執行 npm install,就可以了。

用到 worker 時,只需要 require 它。

const workerCode = require("worker!./worker.js");...const worker = new workerCode();

這樣就初始化了 worker,然後就像上面講的一樣使用 worker。

如果需要使用行內 worker,你需要傳遞 inline 引數給 loader。

const workerCode = require("worker?inline!./worker.js");...const worker = new workerCode();

在 worker 中你也可以 import 模組。

import fibonacci from "./fibonacci.js";...const result = fibonacci(num);

缺點

在 Webpack 中使用 worker 很簡單,但是在使用時也有一些坑值得你注意。

首先,無法將程式碼共用部分提取出來。如果你的 worker 中依賴一段共用程式碼,你只能把程式碼新增到 worker 中,不管其他地方是否也用到同樣的程式碼。而且如果你多個 worker 要用同樣的庫,你也需要在每個 worker 中引入它們。

你可能會想如果你不用 worker-loader,然後用CommonsChunkPlugin指定一個新的入口,可能會解決這個問題。但是不幸的是 worker 不像是瀏覽器 window ,一些 feature 不可用,所以一些程式碼必須要引入。

同時,用行內 worker 也不會解決問題,共用的程式碼依然會出現在多個地方。

第二點缺點是,行內 worker 可能會導致 ObjectURLs記憶體洩露.它們被創建出來以後就不會被釋放。這雖然不是一個大問題,但是如果你有很多“一次性” worker 的話,就會影響效能。

綜上所述,我個人建議是使用標準的 worker,注意在 worker 中引入了什麼。還要注意使用快取。

五、IFrames Web worker

IFrames Web worker 和 IFrame 很像,而且印象中 IFrame 也可以實現多執行緒。但是 IFrame 存在一些不是執行緒安全 API,比如 DOM 相關,瀏覽器不能為他們生成新的執行緒,參考這裡.

在 IFrame 跨域中,很多 API 它都沒有許可權,也只能通過 postMessage,就像 Web Worker 一樣。理論上,瀏覽器可以在不同的執行緒中執行 IFrame,也就可以用 IFrame 實現多執行緒。

但是實際並非如此,它還是單執行緒的,瀏覽器不會給它們額外的執行緒。

總結

Web Worker 解決了 JavaScript 一直以來的大難題,儘管它的語法有些奇怪而且有很多限制,但是它卻可以真真正正的解決問題。從另外一方面來講,它也還是個嬰兒,某些方面還不是很成熟,不能讓我們完全依賴,所以這個技術普及還有一段距離,目前適用場景也比較侷限。所以說,如果你需要做多執行緒,不要再等待其他的什麼技術,學習 web worker 的邊緣問題,避開它的坑,你就可以很好的提高使用者體驗。以上就是這篇文章的全部內容,希望對大家能有所幫助。

TAGS:並行處理