本稿ではTypeScriptでシンプルなScalaのアクター(actor)っぽいものを実装する方法を紹介します。
Scalaのアクターとは
アクターは送られてきたメッセージに対して処理を行うオブジェクトです。普通のオブジェクトのメソッド呼び出しと何が違うのかというと、アクターは固有のメールボックスを持ちます。メッセージボックスはキュー(queue)のようなものです。アクターが受け取ったメッセージは一旦、メールボックスに入れられます。アクターはメッセージボックスから、ひとつひとつメッセージを取り出して、順番に処理します。順番に処理するのも、平行処理ではなく、ひとつひとつ処理していくのが特徴です。
Scalaのアクターはもっと高度で多機能ですが、詳細は割愛しています。上は本稿と関係するところだけをかいつまんでの説明になります。
TypeScriptでアクター的なものを作る意味
JavaScriptは基本的にシングルスレッド1なので、アクターモデルが解決するマルチスレッドでのミュータブルオブジェクトの扱いの難しさみたいなものは、そもそも関係ありません。
JavaScriptでアクター的なものがあると便利なケースは、メソッド呼び出しをロックしないとならないケースです。次のコードのように、ミュータブルなフィールドと、それをいじくるメソッドを持つオブジェクトがあったとします。
class MyObject {
#state // ミュータブルなフィールド
async mutate() {} // stateをいじくるメソッド
}
ミュータブルなステートを安全に操作するには、mutate
メソッドが同時に平行して呼び出されないようになっていたほうが安心です。mutate
メソッドが同時に実行されないようにするには、JavaScriptではawait
を使って呼び出し元で注意するという方法があります。
await myObject.mutate();
await myObject.mutate();
await myObject.mutate();
しかし、これだと、呼び出し元を頼るしかなくなってしまい、防衛的ではありません。
メソッドの使われ方に頼るのではなく、メソッド側で何らかの対策をしたいところです。JavaScriptにもJavaのsynchronized
のようなものがあれば、
class MyObject {
async synchronized mutate() {}
}
のように書くだけで済むのですが、無いので何らかの方法でロック処理を書く必要があります。しかし、JavaScriptでは行儀の良いロック処理を書くのは難しいです。while
などでロック状態を確認すると、他の処理もブロックしてしまいます。
class MyObject {
async mutate() {
// ここにいい感じのロック処理。でもどうやって?
}
}
そこで、アクターです。アクターの特徴のうち、メールボックスのメッセージを順番に処理するような実装が理想に近いと思われます。
TypeScriptでシンプルなアクターを実装する方法
TypeScriptでアクターっぽいものを実装するには、まずメールボックス(キュー)と受け取ったメッセージをメールボックスに追加する機能を実装します。
abstract class Actor {
#tasks = Promise.resolve(); // ❶
tell(message: any): void {
console.log("queue", message);
this.#tasks = this.#tasks.then(() => this.receive(message)); // ❷
}
protected abstract receive(message: any): Promise<void>;
}
#tasks
がメールボックスに当たります(❶)。tell
で受け取ったメッセージは、関数() => this.receive(message)
にくるまれて、#tasks
に追加されます(❷)。
メッセージをメールボックスから取り出して処理を動かす部分は、自前で実装する必要はありません。Promise.prototype.then
自体に受け取った関数を実行する機能が備わっているためです。
アクタークラスを作るには、上のActor
を継承します。
class MyActor extends Actor {
protected async receive(message: any): Promise<void> {
console.log("start task", message);
await sleep(1000);
console.log("done task", message);
}
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
アクターにメッセージを送るには、アクタークラスをインスタンス化して、tell
メソッドを呼び出します。
const actor = new MyActor();
actor.tell(1);
actor.tell(2);
actor.tell(3);
以上のコードを実行すると、タスクがキューされたあとに処理がひとつひとつ行われます。
完全なデモは次のTypeScript Playgroundを御覧ください。
-
Node.jsのworker_threadsやDOMのWeb Workerを使えば、マルチスレッドなJavaScriptも書けます。 ↩