28
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

TypeScriptでシンプルなActorを実装する方法

Last updated at Posted at 2022-01-28

本稿では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);

以上のコードを実行すると、タスクがキューされたあとに処理がひとつひとつ行われます。

20220128_104227.gif

完全なデモは次のTypeScript Playgroundを御覧ください。

  1. Node.jsのworker_threadsDOMのWeb Workerを使えば、マルチスレッドなJavaScriptも書けます。

28
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?