Napa.js 簡介

meikidd發表於2019-03-01

原文地址:github.com/Microsoft/n…

本文介紹 Napa.js 的核心概念,帶領大家探索 Napa.js 是如何運轉起來的。關於它的由來和開發初衷,可以閱讀 這篇文章

簡介

Zone

Zone 是 Napa.js 中的核心概念,它是執行 JavaScript 程式碼的基本單元,所有涉及多執行緒相關的內容都離不開 Zone 這個概念。一個程式可以包含多個 zone,而每個 zone 又由多個 JavaScript Worker 組成。

在 zone 內部的所有 worker 都是相似的:他們載入相同的程式碼,幾乎以相同的方式處理 broadcastexecute 請求,你無法指定執行某一個特定 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 上可以做兩種型別的操作:

  1. Broadcast - 所有 worker 執行同樣的程式碼,改變 worker 狀態,返回 promise 物件。不過我們只能通過 promise 的返回結果判斷執行成功還是失敗。通常用 broadcast 來啟動應用、預載入一些資料或者修改應用設定。
  2. Execute - 在一個隨機 worker 上執行,不改變 worker 狀態,返回一個包含結果資料的 promise。 execute 通常是用來做實際業務的。

Zone 的操作採用“先進先出”的策略,但 broadcastexecute 優先順序更高。

以下程式碼演示了使用 broadcastexecute 完成一個簡單的任務:

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 版本已經修復上述問題。

步驟如下:

  1. 安裝先決依賴

    • Install C++ compilers that support C++14:
      • xcode-select --install
    • Install CMake:
      • brew install cmake
    • Install cmake-js:
      • npm install -g cmake-js
  2. 通過 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('\t' + points
          + '\t\t' + batches
          + '\t\t' + NUMBER_OF_WORKERS
          + '\t\t' + ms
          + '\t\t' + pi.toPrecision(7)
          + '\t' + Math.abs(pi - Math.PI).toPrecision(7));
}

console.log();
console.log('\t# of points\t# of batches\t# of workers\tlatency in MS\testimated π\tdeviation');
console.log('\t---------------------------------------------------------------------------------------');

// 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('\t' + nth
          + '\t' + fibonacci
          + '\t\t' + NUMBER_OF_WORKERS
          + '\t\t' + ms);
}

console.log();
console.log('\tNth\tFibonacci\t# of workers\tlatency in MS');
console.log('\t-----------------------------------------------------------');

// 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複製程式碼