近況つづき

頭が痛いだけでなく, 寝ている時に頭の傷を庇うせいか首がいたい. 鞭打ちかもしらんけど... そして頭とセットで打った臀部もいたい. 満身創痍で出かける気力もなく, 家でうだうだしているところ. こんなに良い天気なのになあ.

うだうだついでに貰った本の紹介.

いただきました. ありがとうございます > 著者およびオーム社の中の方. Erlang の親玉が書いた入門書の翻訳です.

Erlang は言語として特に斬新なところはなく, 処理系の提供するサービスの出来がいい, というのを伝聞で聞いていた. 読んでみるとたしかにそうだった. 本の内容も言語仕様(文法)の話は前半だけ. 後半は分散だとか並列の話をしている. 面白いのは後半.

私はお仕事の関係もあって分散メッセージングの仕組みには少し興味があったから, これはとても勉強になった. actor model とか. 知り合いのゲームプログラマと話すたびによく聞かされるのが "ゲームのオブジェクトは自律的/疎結合 につくっとけ" という話. actor はそういう感じかもしれない. actor (プロセス) の寿命管理の話もなるほどだった. 死亡通知もメッセージにすればいいのねという.

そのほか Mnesia という Erlang 製データベースについて, 問い合わせが list comprehension なのに驚いた. quote もないのにどうやって list comprehension を実際の問合せに使うんだろうと思ったけれど, そこは実装側が query handle という仕組み でがんばるらしい. Expression Template みたいなもんだろうか...

多少言語が地味でも, "分散アプリがちゃちゃっと作れますよ" と言われたら心は揺らぐよね. 私が最近さわった新しい言語は VB と Squirrel で, それぞれ VC++ のマクロとアプリ組み込み用. どちらの言語にも愛はないけれど, 愛がなくても用はあるからさわらざるをえない. 言語にとって処理系やランタイムの出来はとても大事だと痛感.

C++ と Actor

さて, C++ プログラマであるところの私は悩む: C++ の上でどうやって actor をつくったものか. たぶん負け戦だけど, どのへんが負け戦かは知っておいて良かろう.

面倒そうに思えたのは三つ.

coroutine

メッセージの直列化

RPC との相性

どれも C++ プログラマが苦闘して散った分野なのが見てとれる.

coroutine

Erlang の actor は receive() 関数でメッセージが届くのをまつ. 数千の actor が receive() すると, 素朴な実装では数千のスレッドができて, 主記憶を食い潰してしまう. Erlang や Stackless Python のような (scheme も？) まともな actor システムを持つ言語は, そうした問題を回避している. actor 自身が使う stack だけなら大した量じゃないから, その部分だけを switch できれば OS のスタック全体を switch するより軽量で済む, というのが 私の理解. 実装を知らないので違ったらごめんなさい.

C/C++ で coroutine ライブラリというときは, あの手この手でコンテクスト切り替えを実装している. 無理矢理スタックをコピーするものもある. あまり近づきたくない. できれば言語の上だけでなんとかしたい.

Scala Actor は, それをなんとかしたと主張している. JVM 上で動く Scala は C/C++ と同じ OS stack の制限を持つ. どうやってなんとかしたのか. サンプルコード を見てみると...

def act() { var pongCount = 0 loop { react { case Ping => if (pongCount % 1000 == 0) Console.println("Pong: ping "+pongCount) sender ! Pong pongCount = pongCount + 1 case Stop => Console.println("Pong: stop") exit() } } }

react() の引数が block になっている. 一見すると loop() によってループが回りつづけるよるに見えるが, 実際は react() が例外を投げる. 処理はフレームワークに戻り, 受信すべきメッセージが届いたら block の関数オブジェクトが呼び出される. 例外による脱出で continuation を模倣する手法は Java 界隈の標準らしく, Jetty continuation も 似たようなことをしている. ただし関数オブジェクトでの CPS はしない. 例外で脱出せず利用者側が return してもよさそうだけど, そこは趣味の問題と思う.

というわけで, OS スタック依存言語で actor をするには 関数オブジェクトと例外を組合せればいいらしい.

もっとも C++ は関数オブジェクトを "ぱっと見は連続した" コードに見せる block や lambda のような仕組みがない. なので Scala 式 Actor がどれだけ有り難いかはわからない. ぐぐってみつけた C++ による actor 実装 Theron では, actor は react() 相当の処理をするために親クラスの Receive() メソッドを実装する. この方が C++ ユーザ的な馴染みはいいかもしらん.

Scala actor の作者もそうした解決があることは承知している. 解説記事(PDF) によれば, インターフェイスを実装してフレームワークに呼ばせる IoC 形式を 回避した設計を選んだと主張している. 個人的にはどっちでもいいんじゃね, とおもう. それに IoC を回避した結果, 親クラスが fat になってしまうのは嬉しくない.

結論: どれもいまいち.

もっとも lua や Squirrel と繋いでしまえば その上で coroutine が使えるから, 足回りが多少みっともないのは許容できるのかもしれない. 世間のゲーム屋さんはどうしてるんでしょうね.

メッセージの直列化, RPC との相性

あとは細かい話ふたつ. まずメッセージの直列化. Erlang では receive() の引数に任意(たぶん)の Erlang 項を渡せる. 項ってのは lisp のリストみたいなやつ. 分散環境だとこれを適当に直列化して遠くの actor に送ったりする. で, C++ にはそういう軽量な直列化の仕組みがない. boost::serialize を使えばいいという反応はありうるが, 適切な deserializer を選ぶ部分が悩ましい.

そういう問題に対する解の一つが RPC だと言える. ポートに対応する deserizlier と callback を振り分ける仕組みが RPC なわけだから. RPC の末裔はそれなりに広く使われているわけだけれど, actor とはまったく独立している. Erlang の RPC は actor の上に実装されている. できればこういう風にしたい. が, あまり妙案はない.

RPC の serializer はふつうオブジェクトからバイト列への変換を行う. そこにもう一段中間表現を設ければいいのかもしれない. バイト列 -> erlang term/lisp list -> オブジェクト みたいな. C++ からその中間表現を扱う良い API があれば便利なものになるだろう. 効率は落ちそうだけれど.

こうして考えてみると, C++ でも actor model に則った分散環境を 作ることはできそうな気がしてきた. ただ, それが既存の仕組みと比べて有り難いものなのかはわからない. とりあえず分散はあきらめて, インプロセスの actor model を作るくらいが妥当なのかもしれない. 世間でいう task-system の中にも actor ぽいのは多そうだしね.