用 ParallelJS 並行處理 JavaScript

cheung_seol發表於2016-06-25

伴隨 HTML5 湧現出的新事物當中,最酷炫的當屬 Web Workers API 的 Worker 介面。在這以前,我們必須使用一些特殊的技巧,才能向使用者提供響應式的網頁。而 Worker 介面能讓我們建立執行週期長並且計算複雜度高的函式。而且, 通過 Worker 例項,我們想要多少 worker 就可以派生出多少。

我在這篇文章將介紹多執行緒的重要性,以及如何用 ParallelJS 在 JavaScript 中實現多執行緒。

為什麼說多執行緒如此重要?

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

在過去的幾年中,同步多執行緒(SMT)一直是提升現代 CPU 計算效能的關鍵。原因很簡單:依然可以用摩爾定律中關於單位面積上電晶體數量的論斷來解釋。然而頻率縮放卻滯步不前。所以必須使用可用的電晶體。結構化的提升(比如單指令多流資料流)和多核通常是最佳方案。

用 ParallelJS 並行處理 JavaScript

為了實現同步多執行緒,我們需要書寫並行程式碼,這種程式碼並行執行並最終得到一個結果。因為大多數順序程式碼要麼很難轉化成並行程式碼,要麼轉換後效率非常低下,所以我們通常得用一些特殊的演算法。因為根據阿姆達爾定律,速度曲線 S 的表示式如下:

用 ParallelJS 並行處理 JavaScript

其中 N 代表並行 workers (比如程式、核心或者執行緒)的數量,P 代表並行分片。以後可能會有更多依賴於並行演算法的核心架構。在高效能運算 GPU 系統和特殊架構的領域中,比如 Intel Xeon Phi 就是這種平臺的代表:

最後,我們應該區分一般的併發應用或演算法與並行執行之間的區別。並行指的是(彼此可能相關的)一組計算過程的同步處理。相比而言,併發指的是一組獨立執行的程式。

JavaScript 中的多執行緒

我們已經知道在 JavaScript 中通過回撥函式實現併發程式的方法。這種方法現在也可以用來實現非同步程式。

JavaScript 本身是執行在單執行緒上的,通過一種事件輪詢機制實現(通常遵循反應器模式)。比如,我們可以利用這個特點來處理對外部資源的非同步請求。這還能確保預先定義的回撥函式永遠只能在相同的執行執行緒內被觸發。 JavaScript 不存在交叉執行緒異常、競態條件或者其它跟執行緒有關的複雜問題。但儘管如此, JavaScript 並未給我們提供一個實現同步多執行緒的方法。

隨著 Worker 介面的引入,以上難題將迎刃而解。從主要應用的角度看, web worker 的程式碼屬於併發執行的任務。通訊也是按照這種規則去處理的。 messages API 同樣適合包含站點和主機頁面之間的通訊。

比如下面的程式碼在收到訊息時,將向發起方響應一個訊息:

雖然理論上一個 web worker 能派生出有一個 web worker ,但實際上大多數瀏覽器都不允許這麼做。所以實現 web worker 之間通訊的唯一方法就是通過 main 應用。通訊中的訊息併發執行,這樣就實現了只有非同步非阻塞的通訊。最開始這種程式碼可能看上去有點兒奇怪,但優點還是不少的。最重要的是,這種程式碼不受競態條件的限制。

這裡看一個簡單的例子,在後臺計算一連串兒的素數,用兩個引數代表序列的起點和重點。首先建立一個名為 prime.js 的檔案,在檔案中寫入以下程式碼:

 

現在我們只需要在 main 應用中新增下面這段程式碼,就能啟動後臺 worker 。

 

這真是太麻煩了,尤其是還需要引入另一個檔案。儘管這樣能起到一個很好的分離作用,但是對於小型任務來說,這樣做未免顯得太累贅了。好在幸運的是我們還有一個別的方法。來看下面這段程式碼:

當然我們可能想實現一個比這種特殊數字( 13 和 14 )更好的方案,取決於瀏覽器,需要撤銷 Blob 和 createObjectURL 。這裡要解釋一下 fs.substr(13, fs.length – 14) 的作用,它是用來提取函式體的。通過呼叫 toString() 方法把函式宣告轉化成一個字串,並去掉函式本身的簽名,這樣來提取函式。

有沒有一種js庫能幫我們實現並行?

ParallelJS 登場

ParallelJS 現在隆重登場。 ParallelJS 為 web workers 提供了更好更方便的 API 。它包括許多輔助函式和非常有用的抽象。我們從提供一些資料來開始。

 

data 將列印出第一行給出的陣列。這裡還沒有用到“並行”。但是例項 p 包含了一組方法,比如 spawn 方法,用來派生一個新的w web worker 。他將返回一個 Promise, 這讓產生結果變得輕而易舉。

上面這段程式碼的問題在於這個計算過程實際上並不是真正的並行。我們只是建立了一個單一的後臺 worker , 這個後臺 worker 一次性處理所有的陣列。只有當整個陣列處理完以後,才能獲取結果。

更好的解決方案是使用 Parallel 例項的 map 函式。

前一個例子當中的程式碼非常簡單,簡單的讓人難以置信。在實際當中,會涉及到許多操作和函式。我們可以通過 require 函式來引入函式。

reduce 函式用來幫助我們把多個結果整合到一塊。它為收集子結果和一次性執行一組指令提供了一個方便的抽象。

結論

ParallelJS 為我們規避許多可能使用 web worker 時發生的問題提供了一個的非常棒的方法。此外,它還具備一個很棒的 API ,提供了一些有用的抽象和輔助函式。以後 ParallelJS 可能還會有更進一步的優化。

隨著在 JavaScript 中使用同步多執行緒的能力,我們可能同時想要使用向量的能力。 SIMD.js 貌似提供了一種可行的方法。在不遠的以後(希望不會太遙遠),把 GPU 用於計算可能是一個有效的選項。 Node.js 中存在用於 CUDA (一種平行計算的架構)的封裝,但是執行原生的 JavaScript 仍然存在靈活度不夠的問題。

截止本文創作之前, ParallelJS 是我們處理長執行週期計算的最好方法,能充分利用多核 CPU 的強大之處。

那麼你呢?你打算怎麼通過 JavaScript 發揮現代硬體的強大之處?

打賞支援我翻譯更多好文章,謝謝!

打賞譯者

打賞支援我翻譯更多好文章,謝謝!

任選一種支付方式

用 ParallelJS 並行處理 JavaScript 用 ParallelJS 並行處理 JavaScript

相關文章