本文是我翻譯《JavaScript Concurrency》書籍的第五章 使用Web Workers,該書主要以Promises、Generator、Web workers等技術來講解JavaScript并發編程方面的實踐,
完整書籍翻譯地址:https://github.com/yzsunlei/javascript_concurrency_translation ,由于能力有限,肯定存在翻譯不清楚甚至翻譯錯誤的地方,歡迎朋友們提issue指出,感謝,
Web workers在Web瀏覽器中實作了真正的并發,它們花了很多時間改進,現在已經有了很好的瀏覽器支持,在Web workers之前,我們的JavaScript代碼局限于CPU,我們的執行環境在頁面首次加載時啟動,Web workers發展起來后 - Web應用程式越來越強大,他們也開始需要更多的計算能力,與此同時,多核CPU現在很常見 - 即使是在一些低端設備上,
在本章中,我們將介紹Web workers的思想,以及它們如何與我們努力在應用中實作的并發性原則產生關聯,然后,將通過示例學習如何使用Web worker,以便在本書的后面部分,我們可以開始將并發與我們已經探索過的其他一些想法聯系起來,例如promises和generators,
什么是Web workers?
在深入研究實作示例之前,本節將簡要介紹Web workers的概念,搞清楚Web workers如何與引擎下的其他系統協作的,Web workers是作業系統執行緒 - 我們可以調度事件的物件,它們以真正的并發范式來執行我們的JavaScript代碼,
OS執行緒
從本質上講,Web workers只不過是作業系統級執行緒,執行緒有點像行程,除了它們需要更少的開銷,因為它們與創建它們的行程共享記憶體地址,由于為Web workers提供支持的執行緒處于作業系統級別,因此受系統及其行程調度程式的管理,實際上,這正是我們想要的 - 讓內核清楚我們的JavaScript代碼應該什么時候運行,這樣才能充分地利用CPU,
下面的示圖展示了瀏覽器如何將其Web workers映射到OS執行緒,以及這些執行緒如何映射到CPU上:
在日常活動結束時,作業系統最好能放下其他任務來負責它擅長的 - 處理物理硬體上的軟體任務調度,在傳統的多執行緒編程環境中,代碼更接近作業系統內核,Web workers不是這種情況,雖然底層機制是一個執行緒,但是暴露的編程介面看起來更像是你可能在DOM中查找的東西,
事件物件
Web workers實作了熟悉的事件物件介面,這使得Web workers的行為類似于我們使用的其他組件,例如DOM元素或XHR請求,Web workers觸發事件,這就是我們在主執行緒中從他們那里接收資料的方式,我們也可以向Web workers發送資料,這使用一個簡單的方法呼叫,
當我們將資料傳遞給Web workers時,我們實際上會觸發另一個事件;只有這時候,它位于Web workers的執行背景關系中,而不是在主頁面的執行背景關系,沒有更多的事情要處理:資料輸入,資料輸出,沒有互斥結構或任何此類結構,這實際上是一件好事,因為作為平臺的Web瀏覽器已經有許多模塊,想象一下,如果我們投入很復雜的多執行緒模型而不是一個簡單的基于事件物件的方法,我們每天已經有足夠多的bugs需要處理,
以下是關于Web worker排布的樣子,相對于生成這些Web workers的主執行緒:
真正的并發
Web workers是在我們的架構中實作并發原則的方法,我們知道,Web workers是作業系統執行緒,這意味著在它們內部運行的JavaScript代碼可能在與主執行緒中的某些DOM事件處理程式代碼相同的實體上運行,能夠做這樣的事情已經在很長一段時間成為JavaScript程式員的目標了,在Web workers之前,真正的并發性是不可能的,我們所做的最好的就是模擬它,給用戶一種許多事情同時發生的的假象,
但是,始終在同一CPU內核上運行是存在問題的,我們從根本上限制了在給定時間視窗內可以執行多少次計算,當引入真正的并發性時,此限制會被打破,因為可以運行計算的時間視窗會隨著添加的CPU而增加,
話雖這么說,對于我們的應用程式所做的大多數事情,單執行緒模型作業的也很好,現在的機器都很強大,我們可以在很短的時間內完成很多作業,當我們臨近峰值時會出現問題,這些可能是一些事件中斷了我們代碼處理行程,我們的應用程式不斷被要求做得更多 - 更多功能,更多資料,
Web workers所關心的就是我們可以更好地利用我們面前的硬體的方法,Web workers,如果使用得當,它不一定是我們在專案中永遠不會使用的不可逾越的新東西,因為它的概念超出我們之前的理解,
workers的種類
在開發并發JavaScript應用程式中,我們可能會見到三種型別的Web workers,在本節中,我們將比較這三種型別,以便可以了解在給定的背景關系中哪種型別的workers更有用,
專用workers
專用workers可能是最常見的workers型別,它們被作為是Web worker的默認型別,當我們的頁面創建一個新的Web worker時,它專門用于頁面的執行背景關系而不是其他內容,當我們的頁面銷毀時,頁面創建的所有專用workers也會銷毀,
頁面與其創建的任何專用worker之間的通信方式非常簡單,該頁面將訊息發送給workers,workers又將訊息發回頁面,這些訊息的順序取決于我們嘗試使用Web worker解決的問題,我們將在本書中深入研究這些訊息傳遞模式,
術語主執行緒和頁面在本書中是同義詞,主執行緒是典型的執行背景關系,我們可以在這里操作頁面并監聽輸入,
Web worker背景關系基本相同,但只能訪問較少的Web組件,我們將很快討論這些限制,
以下是頁面與專用workers通信的描述:
正如我們所看到的那樣,專用workers是專注的,它們僅用來服務創建它們的頁面,他們不直接與其他Web workers通信,也無法與任何其他頁面進行通信,
子workers
子workers與專用workers非常相似,主要區別在于它們是由專門的Web worker創建的,而不是由主執行緒創建的,例如,如果專用workers的任務可以從并發執行中受益,則可以生成子workers并協調子workers之間的任務執行,
除了擁有不同的創建者之外,子workers還具有一些與專用workers相同的特征,子workers不直接與主執行緒中運行的JavaScript通信,由創建它們的worker來協調他們的通信,以下有張示圖,說明子workers如何按照約定來運行的:
共享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 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 workers通信
前面的示例創建了一個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對我們來說就沒什么用了,在某些時候,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 = https://www.cnblogs.com/yzsunlei/p/e.data;
}
});
//啟動worker開始通信,
worker.port.start();
有趣!現在,如果我們繼續打開兩個或更多瀏覽器選項卡,我們對輸入值的任何更改都將立即反映在其他頁面中,這個設計的優點在于它的表現一致; 無論哪個頁面執行更新,任何其他頁面都會收到更新的資料,換句話說,這些頁面承擔著資料生產者和資料消費者的雙重角色,
您可能已經注意到,最后一個示例中的worker向所有埠發送訊息,包括發送訊息的埠,我們肯定不想這樣做,
為避免向發送方發送訊息,我們需要以某種方式排除for..of回圈中的發送埠,
這實際上并不容易,因為訊息事件物件沒有與一起發送埠的識別資訊,我們可以建立埠識別符號并使??訊息包含ID,
這里需要有很多作業,好處并不是那么好,這里的并發設計 - 只是簡單地檢查頁面代碼,該訊息實際上與頁面相關,
通過子workers執行子任務
我們在本章中創建的所有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,
Web 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中的錯誤和例外來結束本章,在下一章中,我們將討論并發的實際應用 - 我們應該使用并行執行的任務型別,以及實作它的最佳方法,
最后補充下書籍章節目錄
- 《JavaScript并發編程》第一章 JavaScript并發簡介
- 《JavaScript并發編程》第二章 JavaScript運行模型
- 《JavaScript并發編程》第三章 使用Promises實作同步
- 《JavaScript并發編程》第四章 使用Generators實作惰性計算
- 《JavaScript并發編程》第五章 使用Web Workers
- 《JavaScript并發編程》第六章 實用的并發
- 《JavaScript并發編程》第七章 抽取并發邏輯
另外還有講解兩章nodeJs后端并發方面的,和一章專案實戰方面的,這里就不再貼了,有興趣可轉向https://github.com/yzsunlei/javascript_concurrency_translation查看,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/180932.html
標籤:JavaScript
上一篇:vue使用vuex大體結構
下一篇:JavaScript 日期格式
