技術探し

JavaScriptを中心に記事を書いていきます :;(∩´﹏`∩);:

AyoでWorkerの実装が進んでいる

Chrome Dev Summitのため、サンフランシスコで書いています。

さて、最近、Ayo側でWorkerの動きが活発なのでそろそろまとめてみた。
f:id:about_hiroppy:20171023090343p:plain 昨日一日でこれだけ出ている...!

Ayo側

$ ./ayo --version
v9.0.0-pre

github.com

実装者はAnna、過去にNodeにWorkerを入れようという動きもあったため、petkaantonov/io.jsからベースは引用されています。
例えば、initial implementationはこちら

github.com

この間、第一回目のコアチームミーティングが開催されたが、自分は参加してないので何が話し合われたのかわからないです。
後ほど、アップされるとのことなのでそれを待ちます。

github.com

ただ、WorkerがAyoのコアには入っているので、基本的にはStabilityを2まで持っていく方針だと思われます。
Stabilityとは? About this Documentation | Node.js v8.7.0 Documentation

これが最初のNodeとAyoとの技術的な優位性です。

Node側

NodeもAyoからのコミットを入れるかどうかのPRが上がっています。

github.com

ただ今現在、このPRの1つしか存在せず、進んでいないです。
もし、入れたいと思う人がいればここにコメントするとよさそう;)

Workerとは?

独立したスレッドで動作する環境を構築し、それらの間にメッセージチャンネルを構築します。
WorkerはCPU集約型のJavaScriptの操作を実行するのに便利です。
子プロセスやClusterとは違い、それらの間でWorkerはArrayBufferインスタンス間の転送をしたり、 それらの間でSharedArrayBufferインスタンスを共有をすることによりメモリを効率的に共有することが可能です。

Eventの説明は省きます

const worker = require('worker');
const { Worker, isMainThread, postMessage, workerData } = require('worker');

if (isMainThread) {
  module.exports = async function parseJSAsync(script) {
    return new Promise((resolve, reject) => {
      const worker = new Worker(__filename, {
        workerData: script
      });
      worker.on('message', resolve);
      worker.on('error', reject);
      worker.on('exit', (code) => {
        if (code !== 0)
          reject(new Error(`Worker stopped with exit code ${code}`));
      });
    });
  };
} else {
  const { parse } = require('some-js-parsing-library');
  const script = workerData;
  postMessage(parse(script));
}

メソッド・変数

isMainThread

Workerスレッド内で実行されているかを返します。
実行されてなければ、trueを返します。

postMessage(value[, transferList])

Workerスレッド内でのみ、使用可能です。
worker.on('message')を経由し、受信される親スレッドのWorkerインスタンスにメッセージを送信します。

threadId

現在のスレッドのIdを返します。

workerData

Workerコンストラクタに渡されるデータの複製を含む任意のJavaScriptデータ。

クラス

MessageChannel

このインスタンスは、非同期の双方向通信チャネルを示します。
独自のメソッドを持ちません。

const { MessageChannel } = require('worker');

const { port1, port2 } = new MessageChannel();
port1.on('message', (message) => console.log('received', message));
port2.postMessage({ foo: 'bar' });
// prints from L4: received { foo: 'bar' }

上記のコードは、終了しません。(exit)
ずっと待ち続けます。

MessagePort

非同期の双方向通信チャネルの一端を表します。
構造化データ、メモリ領域、および他のMessagePortsを異なるWorkerまたはVMコンテキスト間で転送するために使用できます。 これは、EventEmitterの拡張です。
また、EventTargetsではなくEventEmitterであるMessagePortsを除いて、この実装はブラウザのMessagePortsと同じです。

postMessage(value[, transferList])

Worker

独立したJavaScript実行スレッドを示します。
殆どのAyoのAPIは、この中で実行可能です。

new Worker(filename, options)
filename

filename絶対パスでなければなりません。
options.evaltrueの場合、パスではなくJavaScriptコードを含む文字列となります。

options
  • eval Workerがオンラインになった時に実行されるJavaScript
  • data クローンされ、require('worker').workerDataとして利用可能となるJavaScript値。 複製は、Structured_clone_algorithmと同様に行われできない場合はエラーが飛ばされます(e.g. 関数が含まれている場合)

developer.mozilla.org

  • maxSemiSpaceSize スレッドのヒープのセミスペースのオプションのメモリ制限(MB単位)。
    ほとんどのshort-lived objectsはここに含まれます。

  • maxOldSpaceSize スレッドのメインヒープのオプションのメモリ制限(MB単位)。

const assert = require('assert');
const { Worker, MessageChannel, MessagePort, isMainThread } = require('worker');
if (isMainThread) {
  const worker = new Worker(__filename);
  const subChannel = new MessageChannel();
  worker.postMessage({ hereIsYourPort: subChannel.port1 }, [subChannel.port1]);
  subChannel.port2.on('message', (value) => {
    console.log('received:', value);
  });
} else {
  require('worker').once('workerMessage', (value) => {
    assert(value.hereIsYourPort instanceof MessagePort);
    value.hereIsYourPort.postMessage('the worker is sending this');
    value.hereIsYourPort.close();
  });
}
// prints from L8: received: the worker is sending this
Worker内の大きな違い
  • process.stdinprocess.stdoutprocess.stderrはnullに設定されています
  • domainモジュールは使えません
  • require('worker').isMainThreadfalseに設定されています
  • require('worker').postMessage()が使用可能で、require( 'worker')on( 'workerMessage')がエミットされます
  • process.exit()はプログラム全体を停止せずに単一のスレッドだけを処理し、process.abort()は使用できません
  • process.chdir()及び、グループまたはユーザーIDを設定するプロセスメソッドは使用できません
  • process.env環境変数に対する読み取り専用の参照となります
  • process.titleは変更できません
  • 明示的にWorkerサポートでビルドされていないネイティブのアドオンはロードできません
  • worker.terminate()が呼び出された結果、実行が停止することが可能性があります
  • 親プロセスからのIPCチャネルにはアクセスできません
postMessage(value[, transferList])

require('worker').on( 'workerMessage')を経由し、受信されるメッセージをworkerに送信します。
require('worker').postMessageと違い、require('worker').on( 'workerMessage')を経由することに注意して下さい。
require('worker').postMessageworker.on('message')

terminate([callback])

可能な限り早く、workerスレッド内のすべてのJavaScript実行を止めます。
callbackは終了したら呼び出されます。

threadId

参照されるスレッドIDを返します。

最後に

これらはstabilityが1なのでまだ不安定 + 変更があります。
WorkerはClusterよりもパフォーマンスが良く、利便性があります。
今はAyoだけですが、今後Node側へも入る可能性はあるので注目しておいても良いかもです。