Copyright 2018-2023 Moddable Tech, Inc.
改訂: 2023年8月20日
ModdableランタイムはXSと統合されており、複数の仮想マシンが単一のマイクロコントローラ上で共存できるようになっています。ほとんどのプロジェクトでは単一の仮想マシンのみを使用しますが、複数の仮想マシンを使用することで提供される複数の独立したランタイムコンテキストが有利になる状況もあります。この分離は、例えばユーザーがインストールしたモジュールをコアプロジェクトの機能から完全に分離するために、セキュリティ、プライバシー、および信頼性の理由で役立ちます。もう1つの有用な状況は、一つの仮想マシンでスクリプトがブロッキング操作を実行している間に、別の仮想マシンのスクリプトが完全に応答し続けることを可能にすることです。複数のCPUコアを持つマイクロコントローラでは、ワーカーが並行して実行され、利用可能なCPUパワーを最大限に活用できます。
プロジェクトで複数の仮想マシンを使用する際は注意が必要です。各仮想マシンは追加のRAMを必要とし、RAMはほとんどのマイクロコントローラの展開で最も限られたリソースです。さらに、仮想マシン間の通信の非同期性はシステム全体の複雑さを増します。それでも、特定の状況では複数の仮想マシンを持つことは有用であり、場合によっては不可欠です。このドキュメントの残りの部分では、Moddable SDKを使用して複数の仮想マシンを使用する方法といくつかの実装の詳細について説明します。
Worker
クラスは仮想マシンを操作するためのAPIです。この実装はWebの Web Workers APIに基づいていますが、いくつかの違いがあります:
- 実装はWeb Workers APIの小さなサブセットです。
- ワーカーは常にモジュールから起動され、スクリプトファイルからは起動されません。
- 新しいワーカーのメモリ構成を指定するための追加が行われています。
- ワーカーにメッセージを投稿する際に追加の状況で例外が発生します。
Web Workersに精通している方は、このドキュメントを読んで、ワーカーの使用に関連する実装の違いを理解することを強くお勧めします。
このドキュメントには、Moddable SDKに実装されている Worker
クラスの独立した説明が含まれています。worker
のサンプル は、Worker
クラスを使用する簡単な例です。
スクリプトは Worker
クラスをインポートして、新しいワーカーを作成できるようにします。
import Worker from "worker";
注意: ワーカーの仮想マシンのメモリは、グローバルシステムメモリから割り当てられます。
ワーカーを起動するには、Worker
クラスのインスタンスを作成し、ワーカーが実行を開始するときに呼び出すモジュールの名前を渡します。次の例では、ワーカー開始時に実行されるモジュールは simpleworker
です。
let aWorker = new Worker("simpleworker");
Worker
コンストラクタの呼び出しは、指定されたモジュールの実行が完了した後にのみ戻ります。このステップでワーカーモジュールが例外を生成した場合、例外が伝播されるため、new Worker
の呼び出しが例外をスローします。この動作は、新しいワーカー仮想マシンの初期化が完了するまで、呼び出し元の仮想マシンがブロックされることを意味します。したがって、新しくインスタンス化された仮想マシンで実行される操作は比較的短時間であるべきです。
前の例では、メイン仮想マシンに使用されるデフォルトのメモリ作成構成でワーカーを起動します。これはワーカーにとって十分でない場合や、ワーカーが必要とする以上のRAMを割り当てる場合があります。オプションの構成オブジェクトを使用すると、新しい仮想マシンをインスタンス化するスクリプトがメモリ使用量を設定できます。
let aWorker = new Worker("simpleworker", {
static: 8192,
stack: 64,
heap: {
initial: 64,
incremental: 32
}
});
ワーカーへのメッセージはJavaScriptオブジェクトおよびバイナリデータです。
aWorker.postMessage({hello: "world", count: 12});
aWorker.postMessage(new ArrayBuffer(12));
ワーカーの実装はメッセージを送信するためにXS Marshallingを使用します。これは、Webブラウザのワーカーの実装よりも多くの種類のデータを送信することをサポートしています。オブジェクトがXS Marshallingを使用して送信できない場合、postMessage
は例外をスローします。
メッセージはコピーによって渡されます(SharedArrayBuffer
などのいくつかの例外を除く)ので、メッセージのサイズは実用的な範囲でできるだけ小さくするべきです。メモリ割り当てに失敗した場合、postMessage
は例外をスローします。
ワーカーインスタンスには、ワーカーからのすべてのメッセージを受信するonmessage
関数があります。これは通常、ワーカーが構築された直後に割り当てられます。
aWorker.onmessage = function(message) {
trace(message, "\n");
}
別のアプローチとして、onmessage
関数を含むWorker
のサブクラスを作成する方法があります。これにより、メモリ使用量が少なくなり、若干高速に実行されます。
class MyWorker extends Worker {
onmessage(message) {
trace(message, "\n");
}
}
...
let aWorker = new MyWorker("simpleworker");
ワーカーをインスタンス化するスクリプトは、ワーカーを終了させることができます。
aWorker.terminate();
ワーカーが終了すると、そのワーカーインスタンスに対してこれ以上呼び出しを行うべきではありません。終了したワーカーにメッセージを送信しようとすると、例外が発生します。
Workerコンストラクタが呼び出されると、指定されたパス(前述の例ではsimpleworker
)のモジュールがロードされ、実行されます。ワーカー自体は通常、2つのタスクを実行します。1つ目は初期化で、2つ目はメッセージを受信する関数のインストールです。受信関数はグローバルオブジェクトself
にインストールされ、onmessage
という名前が付けられます。
let count = 1;
let state = INITIALIZED;
self.onmessage = function (message) {
trace(message, "\n");
}
ワーカースクリプトから送信されるメッセージは、ワーカースクリプトに送信されるメッセージと同様に、JavaScriptオブジェクトまたはArrayBuffer
である場合があります。メッセージはグローバルオブジェクトself
のpostMessage
関数を使用して送信されます。
```js
self.postMessage({hello: "from worker", counter: count++});
ワーカースクリプトは、グローバルオブジェクト self
に対して close
を呼び出すことで自己終了します。これは、インスタンス化スクリプトがワーカーインスタンスに対して terminate
を呼び出すのと同等です。
self.close()
Worker
コンストラクタは、新しいワーカーインスタンスを初期化するために使用されるモジュールへのパスを取ります。
let aWorker = new Worker("simpleworker");
オプションの辞書には、新しいワーカーの作成プロパティが含まれています。辞書が提供されない場合、デフォルトのパラメータが使用されます。これらのデフォルトはホストランタイムによって異なるため、常にメモリ構成を提供することをお勧めします。作成プロパティはマニフェストの creation
セクションと同じです。詳細については、マニフェストのドキュメントを参照してください。
let aWorker = new Worker("simpleworker", {
static: 8192,
stack: 64,
heap: {
initial: 64,
incremental: 32
}
});
注意: 以前の
Worker
の実装では、メモリ作成を構成するために異なるプロパティが使用されていました。これらは非推奨となり、ドキュメントには含まれていません。スクリプトを新しい形式に更新することをお勧めします。
モジュールの実行中にエラーが発生したり例外がスローされた場合、Worker
コンストラクタもエラーをスローします。
terminate
関数は、ワーカーインスタンスの実行を即座に終了し、ワーカーが所有するすべてのリソースを解放します。
aWorker.terminate();
ワーカーが終了した後は、それに対してさらに呼び出しを行うべきではありません。
postMessage
関数は、ワーカーへの配信メッセージをキューに入れます。メッセージはJSONでサポートされているもの、バイナリバッファ (TypedArray
, ArrayBuffer
, SharedArrayBuffer
, DataView
) および XS Marshalling でサポートされているその他のものを使用できます。
aWorker.postMessage("hello");
aWorker.postMessage({msg: "hello", when: Date.now()});
aWorker.postMessage(new ArrayBuffer(8));
メッセージは送信された順序で配信されます。
メッセージはコピーによって渡されます(SharedArrayBuffer
などのいくつかの例外を除く)、したがってメッセージのサイズは実用的に小さくするべきです。メモリ割り当てに失敗した場合、postMessage
は例外をスローします。
workerの onmessage
プロパティには、workerからのメッセージを受信する関数が含まれています。
aWorker.onmessage = function(msg) {
trace(msg, "\n");
}
SharedWorker
クラスは、共有仮想マシンを操作するためのAPIです。この実装は、いくつかの違いがあるウェブの Shared Workers APIに基づいています。違いには以下があります:
- メッセージポートの
close
関数はまだ実装されていないため、Shared Workersを終了することはできません。 - Workersは常にモジュールから起動され、スクリプトファイルからは起動されません。
スクリプトは SharedWorker
クラスをインポートして共有workerに接続し、現在インスタンス化されていない場合は共有workerを作成します。
import {SharedWorker} from "worker";
注意: サンプルとドキュメントが必要です。
ホストランタイムに応じて、workersはプリエンプティブにスケジュールされる場合があります(例:他のworkerやメイン仮想マシンと並行して実行される)、または協調的にスケジュールされる場合があります(例:現在の仮想マシンが譲歩した後にのみ他の仮想マシンが実行される)。協調スケジューリングはスクリプトを分離するのに役立ちますが、ある仮想マシンが他の仮想マシンをブロックするのを防ぐことはできません。
Web Worker仕様では、すべてのWorkerがプリエンプティブにスケジュールされることを前提としています。しかし、一部のマイクロコントローラーでは、プリエンプティブスケジューリングが実用的でない(メモリが多く必要)か、ほぼ不可能(ホストRTOSがサポートしていない)です。
Moddable SDKランタイムの各ホストは、複数の仮想マシンをサポートするかどうかを決定します。サポートする場合、プリエンプティブスケジューリングか協調スケジューリングかを決定します。ESP8266ランタイムは協調タスクモデルに基づいて構築されているため、仮想マシンの協調スケジューリングを実装しています。ESP32はプリエンプティブにスケジュールされるRTOSであるFreeRTOS上に構築されているため、プリエンプティブスケジューリングをサポートしています。プロジェクトで複数の仮想マシンを使用するかどうかを決定する際には、ホストランタイムがサポートしているかどうかを確認してください。
XS仮想マシンのデバッガであるxsbug
は、複数の仮想マシンを同時に操作することをサポートしています。各仮想マシンは、ワーカーを初期化するために使用されたモジュールパスの名前で別々のタブに表示されます。
ECMAScript 2016標準には、共有メモリとアトミックのサポートが含まれています。これらは仮想マシン間の効率的な通信のための強力なツールです。XS仮想マシンはこれらの機能を完全に実装しています。これらは一部のマイクロコントローラ(ESP32)でサポートされていますが、すべてではありません(ESP8266)。
Web Workersの実装は、スレッド間でメッセージを渡すためにModdable SDKのネイティブIPC機能であるmodMessagePostToMachine()
を使用します。ESP32では、これはFreeRTOSキューを使用して実装されています。
デフォルトでは、メッセージキューには10個の要素があります。キューがいっぱいの間にメッセージが投稿されると、キューに空きができるまでブロックされます。この動作は、投稿されるメッセージの数が比較的少ないため、一般的にはうまく機能します。送信者と受信者の間で多くのメッセージが送信される場合、デッドロックが発生する可能性があります。必要に応じて役立つ2つのビルドオプションがマニフェストに用意されています。
"defines": {
"task": {
"queueLength": 20,
"queueWait": 100
}
}
queueLength
プロパティは、メッセージキューのサイズを指定された値に変更します。queueWait
プロパティは、指定されたタイムアウト(ミリ秒単位)後にメッセージの投稿が失敗するようにします。このタイムアウト期間後にメッセージをキューに入れることができない場合、postMessage
は例外をスローします。
デフォルトでは、デバッグビルドは queueWait
を1000ミリ秒に設定します。バランスの取れたシステムでは、メッセージは瞬時にキューに入るべきであり、数ミリ秒以上ブロックすることはありません。このデフォルト設定により、メッセージ送信が予想外に長くかかる場合にデッドロックする代わりに例外を投げることで、キュー関連の潜在的な問題をデバッグできます。デフォルトでは、リリースビルドおよび計測ビルドは queueWait
に無限待機を設定しているため、タイムアウトすることはありません。