CONTAINER技術系のこと書きます。アクターモデルについて学ぶPythonアクターモデルというのは、並行処理のプログラミングモデルの一つだ。並行処理という言葉からは、まずマルチスレッドとかをイメージすると思うけど、それよりも抽象度の高い概念となっている。アクターモデルというのはマルチスレッドなどを用いて構築することになる。比較するものかもしれない。そんなアクターモデルは、概念とか使ったときの嬉しさを理解・実感するのがなかなか難しいモデルだとも思う。理由としては、使い始めるまでに必要なコード量が多かったり、それなりの規模のアプリケーションで使わないとメリットが分かりづらい点が挙げられる。これはあくまで主観的なものだけど、アクターモデルをベースに組まれたアプリケーションは規模が大きくなっても並行処理をしているコードが読みやすい。共有メモリやロックを使う場合だと、平行処理できるパートの量と可読性がトレードオフになりがちな気がするのとは対照的な感じがしている。上記の理由としては、それぞれの手法のアプローチが次のように根本的に異なるためだと思う。使ってみることにする。汎用的なアクターモデルを実装するためのパッケージとしては最も有名そうだった。取られているらしいけど、まあ使う分には特に意識する必要はない。再実装したものではない。次のコンポーネントは明示的にサポートしないことが宣言されている。欲しければ自分で作り込まなきゃいけない。並行処理のタスクをネットワーク越しの各ホストに分配するやつ今回使った環境は次の通り。使ってさくっとできる。入っていないときはインストールしよう。仮想環境を使えるようにしておくのがおすすめ。virtualenvそこらへんはお好みで。アクターモデルアクターモデルでは、その名の通りアクターというコンポーネントを組み合わせてアプリケーションを構築していく。アクターという概念を構成する要素は、実のところたったの三つしかない。他のアクターから何らかのデータを受け取るために用意されたメッセージキューが一つ目。その受け取ったデータを渡す先となるメッセージハンドラが二つ目。最後に、メッセージキューからデータを取り出して、それをメッセージハンドラにディスパッチする役目のスレッドが三つ目だ。この三つの要素が、それぞれのアクターインスタンスに必ず備わっている。メッセージキューとスレッドはデフォルトで一つずつ、メッセージハンドラについては自分で何をするか記述する。重要な点としては、アクター同士はメモリなどを共有することがない。することがないというよりも、する必要がない・するとアクターモデルを使う意味が薄れるという方が正しい。アクターモデルでは、何かを共有することなくお互いのメッセージキューにメッセージを放り込み合うことで情報をやり取りして処理を進めていく。いわゆるメッセージパッシングと呼ばれるものだ。アクターの中と外の世界は非同期の壁で断絶されている。アクターの中身を、その外側から同期的に直接触ることはできない。触ることができてしまうと、処理の競合が起こってしまうからだ。処理はキューイングされていつかは実行されるけど、具体的にそれがいつかは分からない状態でアクセスする。説明が長くなったけど、そろそろ具体的なサンプルコードを見ていくことにしよう。使って作る、最も単純なアプリケーションの一つだ。このアプリケーションではメッセージを受け取って、それを出力するだけのアクターを定義している。説明はコードの中にコメントとして記述している。いうクラスを継承して作る。いうメソッドはアクターがメッセージを受け取って、それがディスパッチされたときに発火するハンドラだ。サンプルコードでは、そのアクターを起動してメッセージを送りつけている。メソッドが呼ばれた瞬間からメッセージキューを読み出すスレッドが起動して動き出す。メソッドでメッセージを送ることができる。いうアクターへの間接的な参照になっている点だ。起動したアクターそのものを直接触ることはできず、必ずこの参照を経由してアクセスすることになる。触ることを防ぐことによって、アクターの中で競合を起こさないようにしているのだろう。ちなみに、上記のサンプルコードではメッセージを送りつけた直後にアクターを停止しているけど、本当はちょっと待った方がお行儀が良いのかもしれない。待つというのは、具体的にはメインスレッドをスリープさせるということだ。キューからのメッセージの取り出しおよびハンドラの発火は別スレッドで進むため、すぐに実行される保証がないから。いえ、他にやっていることもないからすぐに処理されるはずで、現実的には問題ないんだけど。上記のサンプルコードを実行すると、次のようになる。使い方の説明に入っていく。先ほどのサンプルコードでは、アクターにメッセージの形で引数を渡した。いえ、起動時の初期パラメータを設定したい場合も多いと思う。そんなときはコンストラクタに引数を用意すれば良い。親クラスのコンストラクタは必ず呼ぶこと。いうより、呼ばないと例外になる。main()コンストラクタに渡す引数はアクターの起動時に渡す。上記のサンプルコードを実行した結果は次の通り。Sekai!コンストラクタで渡したメッセージが使われている。返り値のあるメッセージパッシングこれまでの例では、アクターにメッセージを送っておしまいという、一方的なものだった。いえ、何かメッセージを渡して、その結果として返り値を戻してほしいというのはよくある。メッセージハンドラでは返り値を戻すように作る。返り値を受け取る側では、いくつか受け取り方を選ぶことができる。次のサンプルコードでは、戻り値のあるパターンを実装している。返り値の受け取り側でもいくつかのやり方を試している。main()上記で返り値を受け取るやり方がいくつかあることについて補足しておく。前述した通り、アクターの内外は非同期の壁で断絶されている。メッセージに対する返事が返ってくるタイミングは、将来であることは分かるものの具体的にいつかは分からないのだ。そのため、返事の受け取り方として「返ってくるまですぐにブロックする」か「とりあえず受理された旨の返事だけ受け取る」やり方を選べるわけだ。先ほどのサンプルコードを実行した結果は次の通り。Future-Timeout!アクターの中で発生した例外を処理するもし、アクターの返り値のある処理で例外が発生したことに気づくことができないと大変だ。メソッド経由で呼び出された処理の中で例外が起こったときは、呼び出し元にそれが伝搬するようになっている。次のサンプルコードではメッセージハンドラの中で意図的に例外を上げている。main()上記のサンプルコードを実行した結果は次の通り。trypykka.pyOops!親スレッドが死んだときにアクターのスレッドも停止させたいちなみに、先ほどのサンプルコードで例外をキャッチしないでいるとどうなるか試してみただろうか。実はアプリケーションの実行はそのまま継続することになる。してそんなことが起こるかというと、子スレッドがデーモンスレッドになっていないためだ。アプリケーションは全てのスレッドが停止するまで処理を継続することになる。親スレッドが停止したときに子スレッドも停止させたい、つまり一家心中を図りたいときは次のようにする。main()こうすれば、例外を取りこぼしたことでメインスレッドが停止したときに、そこから起動されたアクターのスレッドも同時に停止する。Oops!アクターのフラグを切り替えて、挙動の違いを確認してみよう。いうメソッドをオーバーライドすることでアクターを実装してきた。実際には、アクタープロキシという機構を使って、アクターのメソッドをあたかも直接呼び出しているような風に処理できる。生成して、あたかもメソッドを直接呼び出しているように扱っている。main()上記のサンプルコードを実行した結果は次の通り。先ほどのサンプルコードでは、アクタープロキシを使って返り値のない場合を扱っていた。今度は返り値のあるときを試してみる。次のサンプルコードではアクタープロキシを使った上で返り値を受け取っている。経由で返り値を受け取ることになる。上記の実行結果は次の通り。World!アクターのアトリビュートにアクタープロキシ経由でアクセスする前述した通り、アクターの中と外は非同期の壁で断絶されていることから、直接同期的に触ることはできない。それをすると、アクターの処理が競合する恐れがあるためだ。すると相当に面倒くさいと思うんだけど、これは定義する必要がない。経由にはなるが、触ることができる。いうアトリビュートを定義している。メインスレッドからは、それをアクタープロキシ経由で読み出している。すればアクセスが非同期に処理されるため、処理が競合することはない。上記の処理結果は次の通り。World!アクターのメンバが持つメソッドをアクタープロキシ経由で呼び出す先ほどのサンプルコードではアクターのメンバがただの文字列だった。次は、これがメソッドを持ったインスタンスだったときの話。インスタンスをメンバに持っている。それを、アクタープロキシ経由で外から呼び出している。いうフラグが必要になる。このフラグを立てることで、アクタープロキシから呼び出すことができるようになる。上記のサンプルコードの実行結果は次の通り。World!アクターモデルを使った並行処理これまでのサンプルコードでは、アクターは定義しているものの平行処理をしていなかった。説明の都合上、仕方なかったとはいえ一体何のためにこんなことを、という印象だったかもしれない。ここからは、ようやくアクターモデルを使って並行処理をしてみる。アクターモデルの並行処理では、基本的にアクターをたくさん用意してタスクをそれぞれのアクターに振り分けていく。アクターには、それぞれメッセージを処理するスレッドが起動しているので、処理をバラバラに並列で進めることができるわけ。時間がかかる、というところはスレッドをスリープすることで表現した。