本文是我翻譯《JavaScript Concurrency》書籍的第五章 使用Web Workers,該書主要以Promises、Generator、Web workers等技術來說解JavaScript併發編程方面的實踐。javascript
完整書籍翻譯地址:github.com/yzsunlei/ja… 。因爲能力有限,確定存在翻譯不清楚甚至翻譯錯誤的地方,歡迎朋友們提issue指出,感謝。java
Web workers在Web瀏覽器中實現了真正的併發。它們花了不少時間改進,如今已經有了很好的瀏覽器支持。在Web workers以前,咱們的JavaScript代碼侷限於CPU,咱們的執行環境在頁面首次加載時啓動。Web workers發展起來後 - Web應用程序愈來愈強大。他們也開始須要更多的計算能力。與此同時,多核CPU如今很常見 - 即便是在一些低端設備上。node
在本章中,咱們將介紹Web workers的思想,以及它們如何與咱們努力在應用中實現的併發性原則產生關聯。而後,將經過示例學習如何使用Web worker,以便在本書的後面部分,咱們能夠開始將併發與咱們已經探索過的其餘一些想法聯繫起來,例如promises和generators。git
在深刻研究實現示例以前,本節將簡要介紹Web workers的概念。搞清楚Web workers如何與引擎下的其餘系統協做的。Web workers是操做系統線程 - 咱們能夠調度事件的對象,它們以真正的併發範式來執行咱們的JavaScript代碼。程序員
從本質上講,Web workers只不過是操做系統級線程。線程有點像進程,除了它們須要更少的開銷,由於它們與建立它們的進程共享內存地址。因爲爲Web workers提供支持的線程處於操做系統級別,所以受系統及其進程調度程序的管理。實際上,這正是咱們想要的 - 讓內核清楚咱們的JavaScript代碼應該何時運行,這樣才能充分地利用CPU。github
下面的示圖展現了瀏覽器如何將其Web workers映射到OS線程,以及這些線程如何映射到CPU上:編程
在平常活動結束時,操做系統最好能放下其餘任務來負責它擅長的 - 處理物理硬件上的軟件任務調度。在傳統的多線程編程環境中,代碼更接近操做系統內核。Web workers不是這種狀況。雖然底層機制是一個線程,可是暴露的編程接口看起來更像是你可能在DOM中查找的東西。json
Web workers實現了熟悉的事件對象接口。這使得Web workers的行爲相似於咱們使用的其餘組件,例如DOM元素或XHR請求。Web workers觸發事件,這就是咱們在主線程中從他們那裏接收數據的方式。咱們也能夠向Web workers發送數據,這使用一個簡單的方法調用。後端
當咱們將數據傳遞給Web workers時,咱們實際上會觸發另外一個事件;只有這時候,它位於Web workers的執行上下文中,而不是在主頁面的執行上下文。沒有更多的事情要處理:數據輸入,數據輸出。沒有互斥結構或任何此類結構。這其實是一件好事,由於做爲平臺的Web瀏覽器已經有許多模塊。想象一下,若是咱們投入很複雜的多線程模型而不是一個簡單的基於事件對象的方法。咱們天天已經有足夠多的bugs須要處理。api
如下是關於Web worker排布的樣子,相對於生成這些Web workers的主線程:
Web workers是在咱們的架構中實現併發原則的方法。咱們知道,Web workers是操做系統線程,這意味着在它們內部運行的JavaScript代碼可能在與主線程中的某些DOM事件處理程序代碼相同的實例上運行。可以作這樣的事情已經在很長一段時間成爲JavaScript程序員的目標了。在Web workers以前,真正的併發性是不可能的。咱們所作的最好的就是模擬它,給用戶一種許多事情同時發生的的假象。
可是,始終在同一CPU內核上運行是存在問題的。咱們從根本上限制了在給定時間窗口內能夠執行多少次計算。當引入真正的併發性時,此限制會被打破,由於能夠運行計算的時間窗口會隨着添加的CPU而增長。
話雖這麼說,對於咱們的應用程序所作的大多數事情,單線程模型工做的也很好。如今的機器都很強大。咱們能夠在很短的時間內完成不少工做。當咱們臨近峯值時會出現問題。這些多是一些事件中斷了咱們代碼處理進程。咱們的應用程序不斷被要求作得更多 - 更多功能,更多數據。
Web workers所關心的就是咱們能夠更好地利用咱們面前的硬件的方法。Web workers,若是使用得當,它不必定是咱們在項目中永遠不會使用的不可逾越的新東西,由於它的概念超出咱們以前的理解。
在開發併發JavaScript應用程序中,咱們可能會見到三種類型的Web workers。在本節中,咱們將比較這三種類型,以即可以瞭解在給定的上下文中哪一種類型的workers更有用。
專用workers多是最多見的workers類型。它們被做爲是Web worker的默認類型。當咱們的頁面建立一個新的Web worker時,它專門用於頁面的執行上下文而不是其餘內容。當咱們的頁面銷燬時,頁面建立的全部專用workers也會銷燬。
頁面與其建立的任何專用worker之間的通訊方式很是簡單。該頁面將消息發送給workers,workers又將消息發回頁面。這些消息的順序取決於咱們嘗試使用Web worker解決的問題。咱們將在本書中深刻研究這些消息傳遞模式。
術語主線程和頁面在本書中是同義詞。主線程是典型的執行上下文,咱們能夠在這裏操做頁面並監聽輸入。 Web worker上下文基本相同,但只能訪問較少的Web組件。咱們將很快討論這些限制。
如下是頁面與專用workers通訊的描述:
正如咱們所看到的那樣,專用workers是專一的。它們僅用來服務建立它們的頁面。他們不直接與其餘Web workers通訊,也沒法與任何其餘頁面進行通訊。
子workers與專用workers很是類似。主要區別在於它們是由專門的Web worker建立的,而不是由主線程建立的。例如,若是專用workers的任務能夠從併發執行中受益,則能夠生成子workers並協調子workers之間的任務執行。
除了擁有不一樣的建立者以外,子workers還具備一些與專用workers相同的特徵。子workers不直接與主線程中運行的JavaScript通訊。由建立它們的worker來協調他們的通訊。如下有張示圖,說明子workers如何按照約定來運行的:
第三類Web worker被稱爲一個共享worker。共享workers被如此命名是由於多個頁面能夠共享這種類型worker的同一個實例。在該頁面能夠訪問一個給定的共享workers實例由同源策略所限制,這意味着,若是一個頁面跟這個worker不一樣域,該worker是不被容許與此頁面通訊的。
共享workers解決的問題與專用workers解決的問題不一樣。將專用workers視爲沒有反作用的函數。你將數據傳遞給它們並得到不一樣的返回數據。將共享workers視爲遵循單例模式的應用程序對象。它們是在不一樣上下文之間共享狀態的方法。所以,例如,咱們不會僅僅爲了處理數字而建立一個共享worker; 咱們可使用一個專用worker。
當內存中的應用程序數據來自同一應用程序的其餘頁面時,咱們使用共享workers就有意義了。想一想用戶在新選項卡中打開連接。這將建立一個新的上下文。這也意味着咱們的JavaScript組件須要經歷獲取頁面所需的全部數據,執行全部初始化步驟等過程。這形成重複和浪費。爲何不經過在不一樣的瀏覽上下文之間共享的方式來保存這些資源呢?如下有個示圖說明來自同一應用程序的多個頁面與共享workers實例通訊:
實際上還有第四種類型稱爲服務workers。這些是共享worker,其中包含與緩存網絡資源和脫機功能相關的其餘功能。服務workers仍處於規範的早期階段,但他們看起來頗有意義。若是服務workers成爲可行的Web技術,咱們今天瞭解的關於共享workers的任何內容都將適用於服務workers。
這裏要考慮的另外一個重要因素是服務workers的複雜性。主線程和服務worker之間的通訊機制涉及使用端口。一樣,在共享workers中運行的代碼須要確保它經過正確的端口進行通訊。咱們將在本章後面更深刻地介紹共享workers的通訊。
Web worker環境與咱們的代碼一般運行的JavaScript環境不一樣。在本節中,咱們將指出主線程的JavaScript環境與Web worker線程之間的主要區別。
對Web workers的一個常見誤解是,它們與默認的JavaScript執行上下文徹底不一樣。確實,他們是不一樣的,但沒有那麼不一樣以致於沒有可比性。也許,正是因爲這個緣由,JavaScript開發人員在可能的時候迴避使用Web worker是有好處的。
明顯的差距是DOM - 它在Web worker執行環境中不存在。它不存在是規範起草者有意識決定的。經過避免DOM集成到worker線程中,瀏覽器提供商能夠避免許多潛在的特殊狀況。咱們都很是重視瀏覽器的穩定性,或者至少咱們應該重視。從Web worker那裏獲取DOM訪問權限真的很方便嗎?咱們將在本書接下來的幾章中看到,workers擅長許多其餘任務,這些任務最終有助於成功實現併發原則。
因爲咱們的Web worker代碼沒有DOM訪問權限,所以咱們不太可能自找麻煩。它實際上迫使咱們去思考爲何咱們要使用Web workers。咱們實際上可能退後一步,從新思考咱們的方法。除了DOM以外,咱們平常使用的大部分功能權限都有,這正是咱們所指望的。這包括在Web workers中使用咱們喜歡的類庫。
有關Web worker執行環境中缺乏功能的更詳細分類,請參閱此頁面
https://developer.mozilla.org/en-US/docs/Web/API/Worker/Functions_
and_classes_available_to_workers
。
咱們毫不會將整個應用程序編寫在一個JavaScript文件中。相反,咱們經過將源代碼劃分爲文件的方式來便於模塊化,從邏輯上能夠將設計分解爲咱們想映射的內容。一樣,咱們可能不但願有由數千行代碼組成的Web workers。幸運的是,Web worker提供了一種機制,容許咱們將代碼導入到咱們的Web worker中。
第一種場景是將咱們本身的代碼導入到一個Web worker上下文。咱們極可能有許多低級別的工具方法是專門針對咱們的應用程序。有很大可能,咱們就須要在兩個環境使用這些工具:一個普通的腳本環境和一個worker線程。咱們想要保持代碼的模塊化,並但願代碼以相同的方式做用於Web workers環境,就像它會在任何其餘環境下運行。
第二種場景是在Web workers中加載第三方庫。這與將咱們本身的模塊加載到Web workers中的原理相同 - 咱們的代碼能夠在任何上下文中使用,但有一些例外,例如DOM代碼。讓咱們看一個建立Web worker並加載lodash庫的示例。首先,咱們將啓動Web worker:
//加載Web worker腳本,
//而後啓動Web worker線程。
var worker = new Worker('worker.js');
複製代碼
接下來,咱們將使用loadScripts()函數將lodash庫導入咱們的庫:
//導入lodash庫,
//讓全局「_」變量在Web worker上下文中可用。
importScripts('lodash.min.js');
//咱們如今能夠在Web worker中使用庫。
console.log('in worker', _.at([1, 2, 3], 0, 2));
//→in worker[1,3]
複製代碼
在開始使用腳本以前,咱們不須要擔憂等待腳本加載 - importScripts()是一個阻塞的操做。
前面的示例建立了一個Web worker,它確實在本身的線程中運行。可是,這對咱們沒有多大幫助,由於咱們須要可以與咱們創造的workers通訊。在本節中,咱們將介紹從Web workers發送和接收消息所涉及的基本機制,包括如何序列化這些消息。
當咱們想要將數據傳遞給Web worker時,咱們使用postMessage()方法。顧名思義,此方法將給定的消息發送給worker。若是在worker中設置了任何消息事件處理程序,它們將響應此調用。讓咱們看一個將字符串發送給worker的基本示例:
//啓動Web worker線程。
var worker = new Worker('worker.js');
//向Web worker發送消息,
//觸發「message」事件處理程序。
worker.postMessage('hello world');
複製代碼
如今讓咱們看看worker經過爲消息對象設置事件處理程序來查看此響應消息:
//爲任何「message」設置事件監聽器
//調度給該worker的事件。
addEventListener('message', (e) => {
//能夠經過事件對象的「data」屬性訪問發送的數據
console.log(e.type, `"${e.data}"`);
//→message 「hello world」
});
複製代碼
addEventListener()函數是在全局專用Web workers環境調用的。 咱們能夠將其視爲Web workers的窗口對象。
從主線程傳遞到worker線程的消息數據要通過序列化轉換。當此序列化數據到達worker線程時,它被反序列化,而且數據可用做JavaScript基本類型。當worker線程想要將數據發送回主線程時,使用一樣的過程。
毋庸置疑,這是一個多餘的步驟,給咱們可能已通過度工做的應用程序增長了開銷。所以,必須考慮在線程之間來回傳遞數據,由於從CPU成本方面來講這不是輕鬆的操做。在本書的Web worker代碼示例中,咱們將消息序列化視爲咱們併發決策過程當中的關鍵因素。
因此問題是 - 爲何要這麼長?若是咱們在JavaScript代碼中使用的worker只是線程,咱們應該在技術上可以使用相同的對象,由於這些線程使用相同的內存地址段。當線程共享資源(例如內存中的對象)時,可能會發生具備挑戰性的資源搶佔狀況。例如,若是一個worker鎖定一個對象而另外一個worker試圖使用它,則這會發生錯誤。咱們必須實現邏輯來優雅地等待對象變得可用,而且咱們必須在worker中實現邏輯來釋放鎖定的資源。
簡而言之,這是一個容易出錯的使人頭痛的問題,若是沒有這個問題咱們會好得多。值得慶幸的是,在僅序列化消息的線程之間沒有共享資源。這意味着咱們在實際傳遞給worker的東西方面受到限制。經驗上是傳遞能夠編碼爲JSON字符串的東西一般是安全的。請記住,worker必須今後序列化字符串重建對象,所以函數或類實例的字符串表示根本將不起做用。讓咱們經過一個例子來看看它是如何工做的。首先,看一個簡單的worker記錄它收到的消息:
//簡單輸出收到的消息。
addEventListener('message', (e) => {
console.log('message', e.data);
});
複製代碼
如今讓咱們看看使用postMessage()能夠序列化哪一種類型的數據併發送給這個worker:
//啓動Web worker
var worker = new Worker('worker.js');
//發送一個普通對象。
worker.postMessage({hello: 'world'});
//→消息{hello:"world"}
//發送一個數組。
worker.postMessage([1, 2, 3]);
//→消息[1,2,3]
//試圖發送一個函數,結果拋出錯誤
worker.postMessage(setTimeout);
//→未捕獲的DataCloneError
複製代碼
咱們能夠看到,當咱們嘗試將函數傳遞給postMessage()時會出現一些問題。這種數據類型一旦到達worker線程就沒法重建,所以,postMessage()只能拋出異常。這些類型的限制可能看起來過於侷限,但它們確實消除了許多可能出現的併發問題。
若是沒有將數據傳回主線程的能力,workers對咱們來講就沒什麼用了。在某些時候,workers執行的任務須要顯示在UI中。咱們可能還記得,worker實例是事件對象。這意味着咱們能夠監聽消息事件,並在workers發回數據時作出相應的響應。能夠將此視爲向workers發送數據的反向。workers經過向主線程發送消息將主線程視爲另外一個workers線程,而主線程則偵聽消息。咱們在上一節中探討的序列化限制在這裏也是同樣的。
讓咱們看一下將消息發送回主線程的一些worker代碼:
//2秒後,使用「postMessage()」函數將一些數據發回給主線程。
setTimeout(() => {
postMessage('hello world');
}, 2000);
複製代碼
咱們能夠看到,這個worker啓動了,2秒後,將一個字符串發送回主線程。如今,讓咱們看看如何在主JavaScript環境中處理這些傳入的消息:
//啓動一個worker線程。
var worker = new Worker('worker.js');
//爲「message」對象添加一個事件偵聽器,
//注意「data」屬性包含實際的消息數據,
//與發送消息給workers的方式相同。
worker.addEventListener('message', (e) => {
console.log('from worker', `"$ {e.data}"`);
});
複製代碼
您可能已經注意到咱們沒有顯式終止任何worker線程。這不要緊。當瀏覽上下文終止時,全部活動工做 線程都將終止。咱們也可使用terminate()方法顯式的終止worker,這將顯式中止線程而無需等待任何 現有代碼執行完成。可是,不多去顯式終止worker。一旦建立,workers一般在頁面整個生命週期內存活。 生成worker不是免費的,它會產生開銷,因此若是可能的話,咱們應該只作一次。
在本節中,咱們將介紹共享workers。首先,咱們將瞭解多個瀏覽上下文如何訪問內存中的相同數據對象。而後,咱們將介紹如何獲取遠程資源,以及如何通知多個瀏覽上下文有關新數據的返回。最後,咱們將瞭解如何利用共享workers來容許瀏覽上下文之間的直接消息傳遞。
考慮下本節用於實驗編碼的高級特性。瀏覽器對共享workers的支持目前還不是很好(只有Firefox和Chrome)。 Web worker仍處於W3C的候選推薦階段。一旦他們成爲推薦併爲共享workers提供了更好的瀏覽器支持, 咱們就可使用它們了。對於額外的意義,當服務workers規範成熟,共享Worker能力將更加劇要。
到目前爲止咱們已經看到了Web workers的序列化機制,由於咱們不能直接從多個線程引用同一個對象。可是,共享worker的內存空間不只限於一個頁面,這意味着咱們能夠經過各類消息傳遞方法間接訪問內存中的這些對象。實際上,這是一個展現咱們如何使用端口傳遞消息的好機會。讓咱們來看看吧。
端口的概念對於共享worker是很必要的。沒有它們,就沒有管理機制來控制來自共享worker的消息的流入和流出。例如,假設咱們有三個頁面使用相同的共享worker,那麼咱們必須建立三個端口來與該workers通訊。將端口視爲workers通往外部世界的入口。這是一個小的間接的過程。
這是一個基本的共享worker,讓咱們瞭解設置這些類型的workers所涉及的內容:
//這是鏈接到worker的頁面之間的共享狀態數據
var connections = 0;
//偵聽鏈接到此worker的頁面,
//咱們能夠設置消息端口。
addEventListener('connect', (e) => {
//「source」屬性表明由鏈接到這個worker頁面建立的消息端口,
//咱們實際上要經過調用「start()」創建鏈接。
e.source.start();
});
//咱們將消息發回頁面,數據是更新的鏈接數。
e.source.postMessage(++connections);
複製代碼
一旦頁面與此worker鏈接,就會觸發一個connect事件。該connect事件具備一個source屬性,這是消息端口。咱們必須經過調用start()來告訴這個worker已準備開始與它通訊。請注意,咱們必須在端口上調用postMessage(),而不是在全局上下文中調用。worker怎麼知道要將消息發送到哪一個頁面?該端口充當worker和頁面之間的代理,以下圖所示:
如今讓咱們看看如何在多個頁面中使用這個共享worker:
//啓動共享worker。
var worker = new SharedWorker('worker.js');
//設置「message」事件處理程序。
//經過鏈接共享worker,咱們其實是在建立一個消息
//發送到消息傳遞端口。
worker.port.addEventListener('message', (e) => {
console.log('connections made', e.data);
});
//啓動消息傳遞端口,
//代表咱們是準備開始發送和接收消息。
worker.port.start();
複製代碼
這個共享worker和專用worker之間只有兩個主要區別。它們以下:
• 咱們有一個port對象,咱們能夠經過發佈消息和附加事件監聽器來與worker通訊。
• 咱們告訴worker咱們已準備好經過調用端口上的start()方法來啓動通訊,就像worker同樣。
將這兩個start()調用視爲共享worker與其客戶端之間的握手。
前面的示例讓咱們瞭解了來自同一應用程序的不一樣頁面如何共享數據,從而無需在加載頁面時分配兩次徹底相同的結構。讓咱們以這個方法爲基礎,使用共享worker來獲取遠程資源,以便與任何依賴它的頁面共享返回的結果。這是worker線程代碼:
//咱們保存鏈接頁面的端口,
//以便咱們能夠廣播消息。
var ports = [];
//從API獲取資源。
function fetch() {
var request = new XMLHttpRequest();
//當接口響應時,咱們只需解析JSON字符串一次,
//而後將它廣播到全部端口。
request.addEventListener('load', (e) => {
var resp = JSON.parse(e.target.responseText);
for (let port of ports) {
port.postMessage(resp);
}
});
request.open('get', 'api.json');
request.send();
}
//當一個頁面鏈接到這個worker時,
//咱們保存到「ports」數組,
//以便worker能夠持續跟蹤它。
addEventListener('connect', (e) => {
ports.push(e.source);
e.source.start();
});
//如今咱們能夠「poll」API,並廣播結果到全部頁面。
setInterval(fetch, 1000);
複製代碼
咱們只是在ports數組中存儲對它的引用,而不是在頁面鏈接到worker時響應端口。這就是咱們如何跟蹤鏈接到worker頁面的方式,這很重要,由於並不是全部消息都遵循命令響應模式。在這種狀況下,咱們但願將更新的API資源廣播到正在監聽它的全部頁面。一個常見的狀況是在同一個應用程序,若是有許多瀏覽器選項卡打開查看同一個頁面,咱們可使用相同的數據。
例如,若是API資源是一個很大的JSON數組須要被解析,若是三個不一樣的瀏覽器選項卡解析徹底相同的數據,則會很浪費資源。另外一個好處是咱們不會輪詢API 3次,若是每一個頁面都運行本身的輪詢代碼就會是這種狀況。當它在共享worker上下文中時,它只發生一次,而且數據被分發到鏈接的頁面。這對後端的負擔也較少,由於整體而言,發起的請求要少得多。咱們如今來看看這個worker使用的代碼:
//啓動共享worker
var worker = new SharedWorker('worker.js');
//監聽「message」事件,
//並打印從worker發回的任何數據。
worker.port.addEventListener('message', (e) => {
console.log('from worker', e.data);
});
//通知共享worker咱們已經準備好了開始接收消息
worker.port.start();
複製代碼
到目前爲止,咱們已經處理過以共享worker中的數據爲中心的數據資源。也就是說,它來自於一個集中的地方,好比做爲一個API,隨後頁面經過鏈接worker來讀取數據。咱們實際上沒有從頁面修改任何的數據。例如,咱們甚至沒有鏈接到後端,鏈接共享worker的頁面也沒有產生任何數據。如今其餘頁面都須要知道這些改變。
可是,讓咱們說用戶切換到其中一個頁面並進行一些調整。咱們必須支持雙向更新。讓咱們來看看如何使用共享worker來實現這些功能:
//保存全部鏈接頁面的端口。
var ports = [];
addEventListener('connect', (e) => {
//收到的消息數據被分發給任何鏈接到此worker的頁面。
//頁面代碼邏輯決定如何處理數據。
e.source.addEventListener('message', (e) => {
for (let port of ports) {
port.postMessage(e.data);
}
});
});
//保存鏈接頁面的端口引用,
//使用「start()」方法開始通訊。
ports.push(e.source);
e.source.start();
複製代碼
這個worker就像是一顆衛星; 它只是將收到的全部內容傳輸到已鏈接的端口。這就是咱們所須要的,爲何還須要更多?咱們來看看鏈接到這個worker的頁面代碼:
//啓動共享worker,
//並保存咱們正在使用的UI元素的引用。
var worker = new SharedWorker('worker.js');
var input = document.querySelector('input');
//每當輸入值改變時,發送輸入值數據
//到worker以供其餘須要的頁面使用。
input.addEventListener('input', (e) => {
worker.port.postMessage(e.target.value);
});
//當咱們收到輸入數據時,更新咱們文字輸入框的值,
//也就是說,除非值已經更新。
worker.port.addEventListener('message', (e) => {
if (e.data !== input.value) {
input.value = e.data;
}
});
//啓動worker開始通訊。
worker.port.start();
複製代碼
有趣!如今,若是咱們繼續打開兩個或更多瀏覽器選項卡,咱們對輸入值的任何更改都將當即反映在其餘頁面中。這個設計的優勢在於它的表現一致; 不管哪一個頁面執行更新,任何其餘頁面都會收到更新的數據。換句話說,這些頁面承擔着數據生產者和數據消費者的雙重角色。
您可能已經注意到,最後一個示例中的worker向全部端口發送消息,包括髮送消息的端口。咱們確定不想這樣作。 爲避免向發送方發送消息,咱們須要以某種方式排除for..of循環中的發送端口。
這實際上並不容易,由於消息事件對象沒有與一塊兒發送端口的識別信息。咱們能夠創建端口標識符並使消息包含ID。 這裏須要有不少工做,好處並非那麼好。這裏的併發設計 - 只是簡單地檢查頁面代碼,該消息實際上與頁面相關。
咱們在本章中建立的全部workers - 專用workers和共享workers - 都是由主線程生成的。在本節中,咱們將討論子workers。它們與專用worker類似,只是建立者不一樣。例如,子worker不能直接與主線程交互,只能經過產生子workers的代理進行交互。
咱們將看看將較大的任務劃分爲較小的任務,而且咱們還將看看圍繞子worker的一些挑戰性問題。
咱們的Web worker的工做是以這樣的方式執行任務,即主線程能夠繼續服務於一些事情,例如DOM事件,而不會中斷。對於Web worker線程來講,某些任務很簡單。它們接受輸入,計算結果,並將結果做爲輸出返回。可是,若是任務很複雜,該怎麼辦?若是它涉及許多較小的分散步驟,須要咱們將較大的任務分解爲較小的任務,該怎麼辦?
像這些任務,經過將它們分解爲更小的子任務是有意義的,這樣咱們就能夠進一步利用全部可用的CPU。然而,將任務分解爲較小的任務自己會致使嚴重的性能損失。若是任務分解放在主線程中,咱們的用戶體驗可能會受到影響。咱們在這裏使用的一種技術涉及啓動一個Web worker,其工做是將任務分解爲更小的步驟,併爲每一個步驟啓動子worker。
讓咱們建立一個在數組中搜索指定項的worker,若是該項存在則返回true。若是輸入數組很大,咱們會將它分紅幾個較小的數組,每一個數組都是並行搜索的。這些並行搜索任務將做爲子worker建立。首先,咱們來看看子worker:
//偵聽傳入的消息。
addEventListener('message', (e) => {
//將結果發回給worker。
//咱們在輸入數組上調用「indexOf()」,尋找「search」數據。
postMessage({
result: e.data.array.indexOf(e.data.search) > -1
});
});
複製代碼
因此,咱們如今有一個子worker能夠獲取一個數組的塊並返回一個結果。這很簡單。如今,對於棘手的部分,讓咱們實現將輸入數組劃分爲較小輸入的worker,而後將其輸入子worker。
addEventListener('message', (e) => {
//咱們將要分紅4個較小塊的數組。
var array = e.data.array;
//大體計算數組四分之一的大小,
//這將是咱們的塊大小。
var size = Math.floor(0.25 * array.length);
//咱們正在尋找的搜索數據。
var search = e.data.search;
//用於在下面的「while」循環將數組分紅塊。
var index = 0;
//一旦被切片,咱們的塊就會去執行。
var chunks = [];
//咱們須要保存對子worker的引用,
//這樣咱們能夠終止它們。
var workers = [];
//這用於統計從子workers返回的結果數
var results = 0;
//將數組拆分爲按比例大小的塊。
while (index < array.length) {
chunks.push(array.slice(index, index + size));
index += size;
}
//若是還有剩下的(第5塊),
//把它放到它以前的塊中。
if (chunks.length> 4) {
chunks[3] = chunks[3].concat(chunks[4]);
chunks = chunks.slice(0, 4);
}
for (let chunk of chunks) {
//啓動咱們的子worker並在「workers」中保存它的引用。
let worker = new Worker('sub-worker.js');
workers.push(worker);
//當子worker有返回結果時。
worker.addEventListener('message', (e) => {
results++;
//結果是「truthy」,咱們能夠發送一個響應給主線程。
//不然,咱們檢查是否所有子workers都返回了。
//若是是這樣,咱們能夠發送一個false返回值。
//而後,終止全部子workers。
if (e.data.result) {
postMessage({
search: search,
result: true
});
workers.forEach(x => x.terminate());
} else if (results === 4) {
postMessage({
search: search,
result: false
});
workers.forEach(x => x.terminate());
}
});
//爲worker提供一大塊數組進行搜索。
worker.postMessage({
array: chunk,
search: search
});
}
});
複製代碼
這種方法的優勢是,一旦咱們獲得了正確的結果,咱們就能夠終止全部現有的子worker。所以,若是咱們執行一個特別大的數據集,就能夠避免讓一個或多個子worker在後臺進行沒必要要的運算。
咱們在這裏採用的方法是將輸入數組切成四個比例(25%)的塊。這樣,咱們將併發級別限制爲四級。在下一章中,咱們將進一步討論細分任務和技巧,以肯定要使用的併發級別。
如今,讓咱們經過編寫一些代碼在頁面上使用這個worker以完成示例:
//啓動worker...
var worker = new Worker('worker.js');
//生成一些輸入數據,一個數字0 - 1041數組。
var input = new Array(1041).fill(true).map((v, i) => i);
//當worker返回時,顯示咱們搜索的結果。
worker.addEventListener('message', (e) => {
console.log(`${e.data.search} exists?`, e.data.result);
});
//搜索一個存在的項。
worker.postMessage({
array: input,
search: 449
});
//→449存在?真
//搜索一個不存在的項。
worker.postMessage({
array: input,
search: 1045
});
//→1045存在?假
複製代碼
咱們可以與worker通訊,傳遞輸入數組和數據進行搜索。結果傳遞給主線程,它們包含搜索詞,所以咱們可以經過發送給worker線程的原始消息對輸出進行協調。然而,這裏有一些困難須要克服。雖然這很是有用,可以細分任務以更好地利用多核CPU,但涉及到不少複雜性。一旦咱們獲得每一個子worker的結果,咱們就必須進行協調。
若是這個簡單的例子能夠變得像它同樣複雜,那麼想象一下大型應用程序的上下文中的相似代碼。咱們能夠從兩個角度解決這些併發問題。首先是關於併發的前期設計挑戰。這將在下一個章節解決。而後,還有是同步挑戰,咱們如何避免回調地獄?這個話題比較深,將在「第7章,抽取併發邏輯」討論。
雖然前面的示例是一種強大的併發技術,能夠提供很大的性能提高,但還有一些問題須要注意。所以,在深刻涉及子worker的實現以前,請考慮其中的一些挑戰以及必須作出的權衡。
子workers沒有一個父頁面來直接通訊。這是一個複雜的設計,由於即便一個來自子worker簡單響應也須要子worker經過代理從而在運行的JavaScript主線程進行建立。而這樣作獲得的是一堆讓人困惑的通訊過程。換句話說,它很容易致使複雜化的設計,由於要經過比實際上須要的更多組件來完成。因此,在決定使用子workers做爲設計選項以前,讓咱們看看是否能夠只依賴於專用worker來實現。
第二個問題是,因爲Web worker仍然是候選推薦的W3C規範,並不是全部瀏覽器都能一致的實現Web worker的全部功能。共享workers和子workers是咱們可能遇到跨瀏覽器問題的兩個部分。另外一方面,專用workers具備很好的瀏覽器支持,而且在大部分瀏覽器中表現一致。再一次說明,從簡單的專用worker設計開始,若是這不知足須要,再考慮引入共享workers和子workers。
本章中的全部代碼都假設咱們的worker程序中運行的代碼不會出錯。顯然,咱們的workers會遇到異常被拋出的狀況,或者是咱們在開發過程當中編寫有bug的代碼 - 這是咱們做爲程序員所必須面臨的事實。可是,若是沒有適當的錯誤事件處理程序,Web worker可能很難調試。咱們能夠採起的另外一種方法是顯式發回一條消息,標識本身已經出錯。咱們將在本節中介紹兩個錯誤處理話題。
假設咱們的主應用程序代碼向worker線程發送消息,並指望獲得一些返回結果。若是出現問題,那麼等待數據的代碼須要知道該怎麼辦?一種可能性是仍然發送主線程指望的消息; 只是它有一個字段表示操做錯誤的狀態。下圖讓咱們瞭解下它是怎麼樣的:
如今讓咱們看一下實現這種方法的代碼。首先,worker肯定消息返回成功或錯誤狀態:
//當消息返回時,檢查提供的消息數據是不是一個數組。
//若是不是,返回一個設置了「error」屬性的數據。
//不然,計算並返回結果。
addEventListener('message', (e) => {
if(!Array.isArray(e.data)) {
postMessage({
error: 'expecting an array'
});
} else {
postMessage({
error: e.data[0]
});
}
});
複製代碼
該worker老是會經過發送一個消息進行響應,但它並不老是返回一個計算結果。首先,它會檢查,以確保該輸入值是能夠接受的。若是沒有獲得指望的數據,它發送一個附加錯誤狀態的消息。不然,它正常的發送返回結果。如今,讓咱們編寫一些代碼來使用這個worker:
//啓動worker
var worker = new Worker('worker.js');
//監聽來自worker的消息。
//若是收到錯誤,咱們會記錄錯誤信息。
//不然,咱們記錄成功的結果。
worker.addEventListener('message', (e) => {
if (e.data.error) {
console.error(e.data.error);
} else {
console.log('result', e.data.result);
}
});
worker.postMessage([3, 2, 1]);
//→result 3
worker.postMessage({});
//→expecting an array
複製代碼
即便咱們在上一個示例中明確檢查了workers程序中的錯誤狀況,也可能會拋出異常。從咱們的主應用程序線程的角度來看,咱們須要處理這些未捕獲類型的錯誤。若是沒有適當的錯誤處理機制,咱們的Web workers將悄然無聲地失敗。有時候,workers甚至都不加載 - 遇到這種悄無聲息的代碼調試。
咱們來看一個偵聽Web worker error事件的示例。這是一個Web worker嘗試訪問不存在的屬性:
//當一個消息數組返回時,
//發送一個包含的「name」屬性輸入數據做爲響應,
//若是數據沒有定義怎麼辦?
addEventListener('message', (e) => {
postMessage(e.data.name);
});
複製代碼
這裏沒有錯誤處理代碼。咱們所作的只是經過讀取name屬性並將其發回來做爲響應消息。讓咱們看一下使用這個worker的一些代碼,以及它如何響應這個worker中引起的異常:
//啓動咱們的worker
var worker = new Worker('worker.js');
//監遵從worker發回的消息,
//並打印結果數據。
worker.addEventListener('message', (e) => {
console.log('result', `"${e.data}"`);
});
//監遵從worker發回的錯誤,
//並打印錯誤消息。
worker.addEventListener('error', (e) => {
console.error(e.message);
});
worker.postMessage(null);
//→Uncaught TypeError:Cannot read property "name" of null
worker.postMessage({name: 'JavaScript'});
//→result "JavaScript"
複製代碼
在這裏,咱們能夠看到的是第一個發佈消息的worker致使異常被拋出。然而,此異常被封裝在worker內部,它不是拋出在咱們的主線程。若是咱們在主線程監聽error事件,咱們就能夠作出相應的響應。在這裏,咱們只是打印錯誤消息。然而,在其餘狀況下,咱們可能須要採起更復雜的糾正措施,例如釋放資源或發送一個不一樣的消息給worker。
在本章中,咱們介紹了使用Web worker併發執行的概念。在Web worker以前,咱們的JavaScript沒法利用當今硬件上的多核CPU。
咱們首先對Web worker進行了大體的概述。它們是操做系統級的線程。從JavaScript的角度來看,它們是能夠發送消息和監聽message事件的事件對象。Web worker主要分爲三種 - 專用workers,共享workers和子workers。
而後,學習瞭如何經過發送消息和監聽事件來與Web worker進行通訊。而且瞭解到,在消息中傳遞的內容方面存在限制。這是由於全部消息數據都在目標線程中被序列化和重建。
咱們以如何處理Web worker中的錯誤和異常來結束本章。在下一章中,咱們將討論併發的實際應用 - 咱們應該使用並行執行的任務類型,以及實現它的最佳方法。
另外還有講解兩章nodeJs後端併發方面的,和一章項目實戰方面的,這裏就再也不貼了,有興趣可轉向github.com/yzsunlei/ja…查看。