本文介紹 Napa.js 的核心概念,帶領大家探索 Napa.js 是如何運轉起來的。關於它的由來和開發初衷,可以閱讀 這篇文章
簡介
Zone
Zone 是 Napa.js 中的核心概念,它是執行 JavaScript 程式碼的基本單元,所有涉及多執行緒相關的內容都離不開 Zone 這個概念。一個程式可以包含多個 zone,而每個 zone 又由多個 JavaScript Worker 組成。
在 zone 內部的所有 worker 都是相似的:他們載入相同的程式碼,幾乎以相同的方式處理 broadcast
和 execute
請求,你無法指定執行某一個特定 worker 中的程式碼。在不同 zone 之間的 worker 是完全不同的:他們載入不同的程式碼,或者雖然載入相同程式碼但以不同的策略執行,例如堆疊大小不同、安全策略不同等。應用會利用多個 zone 來載入不同的策略。
有兩種型別的 zone:
- Napa zone – 由多個 Napa.js 管理的 JavaScript worker 組成。Napa zone 內的 worker 支援部分 Node.js API
- Node zone – 暴露了 Node.js event loop 的虛擬 zone,具有完備的 Node.js 能力
這樣劃分讓你既可以用 Napa zone 處理繁重的計算事務,也可以用 Node zone 處理 IO 事務。同時 Node zone 也是對 Napa zone 無法完整支援 Node API 的一種補充。
以下程式碼建立了一個包含 8 個 worker 的 Napa zone:
var napa = require(`napajs`);
var zone = napa.zone.create(`sample-zone`, { workers: 8 });複製程式碼
以下程式碼演示如何訪問 Node zone:
var zone = napa.zone.node;複製程式碼
在 zone 上可以做兩種型別的操作:
- Broadcast – 所有 worker 執行同樣的程式碼,改變 worker 狀態,返回 promise 物件。不過我們只能通過 promise 的返回結果判斷執行成功還是失敗。通常用
broadcast
來啟動應用、預載入一些資料或者修改應用設定。 - Execute – 在一個隨機 worker 上執行,不改變 worker 狀態,返回一個包含結果資料的 promise。
execute
通常是用來做實際業務的。
Zone 的操作採用“先進先出”的策略,但 broadcast
比 execute
優先順序更高。
以下程式碼演示了使用 broadcast
和 execute
完成一個簡單的任務:
function foo() {
console.log(`hi`);
}
// This setups function definition of foo in all workers in the zone.
zone.broadcast(foo.toString());
// This execute function foo on an arbitrary worker.
zone.execute(() => { global.foo() });複製程式碼
資料傳輸
由於 V8 不適合在多個 isolate 間執行 JavaScript 程式碼,每個 isolate 管理自己內部的堆疊。在 isolate 之間傳遞值需要封送/拆收(marshalled/unmarshalled),載荷的大小和物件複雜度決定著通訊效率。所有 JavaScript isolate 都屬於同一個程式,且原生物件可以被包裝成 JavaScript 物件,我們嘗試在此基礎上為 Napa 設計一種高效傳輸資料的模式。
為了實現上述模式,引入了以下概念:
可傳輸型別
可傳輸型別是指可以在 worker 中自由傳輸的 JavaScript 型別。包括
- JavaScript 基礎型別:null, boolean, number, string
- 實現了
Transportable
介面的物件(TypeScript class) - 由以上型別構成的陣列或物件
- 還有 undefined
跨 worker 儲存
Store API 用於在 JavaScript worker 中共享資料。當執行 store.set
時,資料被封送到 JSON 並儲存在程式的堆疊中,所有執行緒都可以訪問;當執行 store.get
時,資料被拆收出來。
以下程式碼演示如何利用 store 共享資料:
var napa = require(`napajs`);
var zone = napa.zone.create(`zone1`);
var store = napa.store.create(`store1`);
// Set `key1` in node.
store.set(`key1`, {
a: 1,
b: "2",
c: napa.memory.crtAllocator // transportable complex type.
};
// Get `key1` in another thread.
zone.execute(() => {
var store = global.napa.store.get(`store1`);
console.log(store.get(`key1`));
});複製程式碼
儘管很方便,但不建議在同一個事務裡用 store 傳值,因為這樣做不僅僅只傳輸了資料(還附帶了別的事情,比如加鎖)。另外,雖然有垃圾回收機制,但開發者還是應當在使用完資料後手動刪除相應的 key。
安裝
執行 npm install napajs
安裝。
在 OSX 系統安裝後執行會報錯,在 github issue 中裡也有同樣的提問,解決方法是按照官方的構建文件,自己手動構建。 最新版 v0.1.4 版本已經修復上述問題。
步驟如下:
-
安裝先決依賴
- Install C++ compilers that support C++14:
xcode-select --install
- Install CMake:
brew install cmake
- Install cmake-js:
npm install -g cmake-js
- Install C++ compilers that support C++14:
-
通過 npm 構建
npm install --no-fetch
快速上手示例
計算圓周率 π 值
下面是一個計算 π 值的例子,演示瞭如何利用多執行緒執行子任務。
var napa = require("napajs");
// Change this value to control number of napa workers initialized.
const NUMBER_OF_WORKERS = 4;
// Create a napa zone with number_of_workers napa workers.
var zone = napa.zone.create(`zone`, { workers: NUMBER_OF_WORKERS });
// Estimate the value of π by using a Monte Carlo method
function estimatePI(points) {
var i = points;
var inside = 0;
while (i-- > 0) {
var x = Math.random();
var y = Math.random();
if ((x * x) + (y * y) <= 1) {
inside++;
}
}
return inside / points * 4;
}
function run(points, batches) {
var start = Date.now();
var promises = [];
for (var i = 0; i < batches; i++) {
promises[i] = zone.execute(estimatePI, [points / batches]);
}
return Promise.all(promises).then(values => {
var aggregate = 0;
values.forEach(result => aggregate += result.value);
printResult(points, batches, aggregate / batches, Date.now() - start);
});
}
function printResult(points, batches, pi, ms) {
console.log(` ` + points
+ ` ` + batches
+ ` ` + NUMBER_OF_WORKERS
+ ` ` + ms
+ ` ` + pi.toPrecision(7)
+ ` ` + Math.abs(pi - Math.PI).toPrecision(7));
}
console.log();
console.log(` # of points # of batches # of workers latency in MS estimated π deviation`);
console.log(` ---------------------------------------------------------------------------------------`);
// Run with different # of points and batches in sequence.
run(4000000, 1)
.then(result => run(4000000, 2))
.then(result => run(4000000, 4))
.then(result => run(4000000, 8))複製程式碼
執行結果如下,當設定為 1 組、2 組、4 組子任務平行計算時,可以看出執行時間有明顯提升,當設定為 8 組子任務平行計算時,由於沒有更多的空閒 worker 資源,也就沒有明顯的執行時間的提升。
# of points # of batches # of workers latency in MS estimated π deviation
---------------------------------------------------------------------------------------
40000000 1 4 1015 3.141619 0.00002664641
40000000 2 4 532 3.141348 0.0002450536
40000000 4 4 331 3.141185 0.0004080536
40000000 8 4 326 3.141620 0.00002724641複製程式碼
計算斐波那契數列
var napa = require("napajs");
// Change this value to control number of napa workers initialized.
const NUMBER_OF_WORKERS = 4;
// Create a napa zone with number_of_workers napa workers.
var zone = napa.zone.create(`zone`, { workers: NUMBER_OF_WORKERS });
/*
Fibonacci sequence
n: | 0 1 2 3 4 5 6 7 8 9 10 11 ...
-------------------------------------------------------------------------
NTH Fibonacci: | 0 1 1 2 3 5 8 13 21 34 55 89 ...
*/
function fibonacci(n) {
if (n <= 1) {
return n;
}
var p1 = zone.execute("", "fibonacci", [n - 1]);
var p2 = zone.execute("", "fibonacci", [n - 2]);
// Returning promise to avoid blocking each worker.
return Promise.all([p1, p2]).then(([result1, result2]) => {
return result1.value + result2.value;
});
}
function run(n) {
var start = Date.now();
return zone.execute(``, "fibonacci", [n])
.then(result => {
printResult(n, result.value, Date.now() - start);
return result.value;
});
}
function printResult(nth, fibonacci, ms) {
console.log(` ` + nth
+ ` ` + fibonacci
+ ` ` + NUMBER_OF_WORKERS
+ ` ` + ms);
}
console.log();
console.log(` Nth Fibonacci # of workers latency in MS`);
console.log(` -----------------------------------------------------------`);
// Broadcast declaration of `napa` and `zone` to napa workers.
zone.broadcast(`
var napa = require("napajs");
var zone = napa.zone.get("zone");
`);
// Broadcast function declaration of `fibonacci` to napa workers.
zone.broadcast(fibonacci.toString());
// Run fibonacci evaluation in sequence.
run(10)
.then(result => { run(11)
.then(result => { run(12)
.then(result => { run(13)
.then(result => { run(14)
.then(result => { run(15)
.then(result => { run(16)
}) }) }) }) }) })複製程式碼
運算結果
Nth Fibonacci # of workers latency in MS
-----------------------------------------------------------
10 55 4 10
11 89 4 13
12 144 4 15
13 233 4 22
14 377 4 31
15 610 4 50
16 987 4 81複製程式碼