Pixel Pedals of Tomakomai

北海道苫小牧市出身の初老の日常

POEのコードをAnyEventのコードにしてみる

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

Twitter / Daisuke Murase

@typester やはり定石としてはそうなんですかね。weakenし過ぎて、イベントループから呼んだときはundefになっちゃってるってのを何度かやりました・・・。

Twitter / hiratara

*1:ただ、クドい書き方であまりAnyEventっぽくないです。

*2:-w で動かしていると Argument "POE::Kernel" isn't numeric in numeric ne (!=) at .../POE/Resource/Sessions.pm line 510. と言われるかも。AnyEvent::Impl::POE で $poe_kernel->loop_do_timeslice; ではなく POE::Kernel->loop_do_timeslice; でループさせてるせい。

*3:追記(weakenの話)も参照するとよさげ。