POEからAnyEventに移すのはどんな感じか、試しにやってみます。
最初にまとめ
恐らく問題となるのはPOEのイベント呼び出しとAnyEventのコールバックの仕組みのギャップですが、AnyEventではメソッドを呼び出すコールバックを渡すことでPOEのイベント呼び出しが表現できます*1。
特にpostやyieldに関しては、POE::Sessionの持つHEAPをオブジェクトにして、AE::timerでメソッドを遅延呼び出しすれば同等の処理を表現できます。
0. 題材
以下のような単純なPOEのコードをAnyEventにしましょう。
use strict; use warnings; use POE; POE::Session->create( inline_states => { _start => sub { print "Started.\n"; $_[HEAP]->{counter} = 0; $_[KERNEL]->yield('countup'); }, countup => sub { print "COUNT ", ++$_[HEAP]->{counter}, ".\n"; return if $_[HEAP]->{counter} >= 10; $_[KERNEL]->yield('countup'); }, _stop => sub { print "Finished.\n"; }, } ); POE::Kernel->run;
1. ループをAnyEventにする
POE::Kernel->runを、以下のようにします。
use AnyEvent::Impl::POE (); use AnyEvent (); use POE; ... 中略 ... my $cv = AE::cv; $cv->recv;
これだけloop_do_timeslice; ではなく POE::Kernel->loop_do_timeslice; でループさせてるせい。">*2。ただし、この状態ではループが止まりません。ループの停止はちょっと面倒なので後にして、今はCtrl+cとかで凌いで下さい。
ここで重要なのは、AnyEventとPOEのコードが混在していることです。つまり、POEからAnyEventへの乗り換えは一度に全部やる必要はなく、書き換えやすい部分から徐々に乗り換えていくことができます(今回は関連するPOE::Sessionが一つなんで一度にやりますけど)。
2. Sessionをオブジェクトにする
(自分が普段はMooseX::POEを使ってるからってのも理由の一つですが)セッションはHEAPという状態を持っているので、そのままオブジェクトにしてしまいます。
_startは他のPOE::Sessionが出そろってから呼ばれることを想定しているコードもあると思い、今回はコンストラクタではなくstartメソッドにしました。場合によってはコンストラクタでもいいと思います。_stopは呼ばれるタイミングが解放時なんで、DESTROYにしてます。
package Counter; use Moose; use AnyEvent (); has counter => ( isa => 'Int', is => 'rw', default => 0, ); sub start { my $self = shift; print "Started.\n"; $_[KERNEL]->yield('countup'); } sub countup { my $self = shift; print "COUNT ", $self->counter( $self->counter + 1 ), ".\n"; return if $self->counter >= 10; $_[KERNEL]->yield('countup'); } sub DESTROY { print "Finished.\n"; } __PACKAGE__->meta->make_immutable; no Moose;
ここではまだyieldが残っているので、このままでは動きません。
3. yieldとpostをAE::timerで書き換え
yieldやpostはイベントループへメソッド呼び出し(≒イベントの通知)を預けるものなので、AE::timerで書き換えます*3。
sub start { my $self = shift; print "Started.\n"; my $t; $t = AE::timer 0, 0, sub { $self->countup; undef $t; }; } sub countup { my $self = shift; print "COUNT ", $self->counter( $self->counter + 1 ), ".\n"; return if $self->counter >= 10; my $t; $t = AE::timer 0,0, sub { $self->countup; undef $t; }; }
ちなみに、今回の例くらいだとAE::timerを噛ませずにそのまま$self->countupでも大丈夫です(その場合再帰が終わるまでイベントループに処理が戻りませんが)。こうすると当然$poe_kernel->callと同等の効果になります。
4. 初期化のコードを書く
POE::Session->createの部分を作ったオブジェクトで書き換えます。startだけイベントループから遅れて呼び出されるようにします。
package main; { my $counter = Counter->new; my $t; $t = AE::timer 0, 0, sub { $counter->start; undef $t; }; } my $cv = AE::cv; $cv->recv;
6. プログラムが終了するようにする
POEではSessionが全部無くなれば終わりますが、AnyEventではそんな仕組みはありません。Counterクラスにコールバックを渡せるようにし、処理が終わったらコールバックを呼んでもらうようにします。
package Counter; has cb => ( isa => 'CodeRef', is => 'ro', default => sub { sub {} }, ); ... 略 ... sub DESTROY { my $self = shift; print "Finished.\n"; $self->cb->(); } package main; my $cv = AE::cv; { my $counter = Counter->new( cb => sub { $cv->send } ); my $t; $t = AE::timer 0, 0, sub { $counter->start; undef $t; }; } $cv->recv;
7. 後は好きなループを使う
これでコードは全てAnyEventになり、POEに依存する部分は無くなりました。AnyEvent::Impl::POEのままPOEで動かすも良し、use EVするも良し、自由にして下さい。
最終的にできたコードはこちらのgistです。
注意点
今回はなるべく機械的に書き換えようとしたので、出来たコードはあまりAnyEventぽくありません。既存のコードを捨てて刷新できるなら、その方が楽に書き直せるかもしれません。
なお、この書き換えだと頻繁に$selfをクロージャに渡すことになります。なので、クロージャが解放されないと$selfが解放されず、その結果としてDESTORYが呼ばれずにコールバックを呼ぶことができません。循環参照には十分気をつけるとともに、できればコールバックをするタイミングはDESTROYは避けることをお薦めします。今回の例だと、カウンタが10を超えた時点でFinishedを表示してコールバックすることができます。
後、当たり前ですがIOやネットワークを使う場合は対応するAnyEventの機能を使うことになります。基本的には、POEでは特定のsidのイベントを呼んでいる箇所を、AnyEventではオブジェクトにして sub { $sid->event } のようなコールバックを渡すようにすればほぼ書き換えられます。ただし、PoCoやWheelと機能が違うこともありますので、足りない部分は補ったりする必要はあります。
10/2追記
typesterさんより。$selfはweakenして突っ込んだ方が安全そうです。
@hiratara $selfをクロージャに突っ込むときはweakenしとけばおっけーですよ http://bit.ly/FNHFM
@typester やはり定石としてはそうなんですかね。weakenし過ぎて、イベントループから呼んだときはundefになっちゃってるってのを何度かやりました・・・。