モダンPerlの世界へようこそ

第13回AnyEvent:イベント駆動モジュールの方言を吸収する

イベントループを持つモジュールが抱える問題点

イベントループを持つモジュールの一例として、前回POEを取り上げましたが、もちろん同じようなループを持つモジュールはほかにもあります。

たとえば1995年に故ニック・イング・シモンズ(Nick Ing-Simmons)氏が始めたPerl/Tkや、POE誕生前夜の1997年から開発が行われているGtk(のちのGtk-Perl⁠、その後継にあたるGtk2/GlibのPerlバインディング(2003年)など、GUIアプリケーション関連のツールがそうですし、IO::Poll(1997年)IO::Async(2007年)のようなモジュールにもイベントを監視するためのループが使われています。また、Event(1997年)やlibeventのラッパであるEvent::Lib(2004年⁠⁠、EV(2007年)のように、イベントループそのものを実装するためのモジュールを使えば、自分でイベントループを持つモジュール/アプリケーションをつくることもできます。

ただし、このようなモジュールがイベントを監視し続けるために使う「メイン」ループには、同時にひとつしか存在しえないという制約があります。そのため、複数のイベントモジュールを同時に扱うことは、できないか、できても非常に面倒なことになるのが常でした。

たとえば、Tkを使ったGUIアプリケーションの裏で時間のかかる処理を非同期に(GUI部分を操作不能にしないように)実行しようと思ったら、前回紹介したように、ふつうは処理ばらして、ときどきTkのイベントループに処理を戻すような形で書くことになりますが、そうすると、既存の(実績があってもTkのことを考慮していない)モジュールは使いづらくなってしまいますし、アプリケーションを別のイベントモジュール、たとえばGtk2/Glibを使うように移植するときにはその非同期処理も同様にTkベースからGtk2/Glibベースに移植しなおさなければならない、といった問題が起きてしまいます。

このような無駄な手間をなくすために生まれたのが、2004年にマーク・レーマン(Marc Lehmann)氏が開発を始めたAnyEventです。

GUIアプリのための?AnyEvent

連載第4回で取り上げたAny::Moose同様、AnyEventにも「どんなイベントモジュールにも対応できる」という含みがあるのですが、ほかのAny系のモジュールと違って、AnyEventは、本質的には自前のメインループを持つイベントモジュールではありません。ループそのものは別の(既存の)イベントモジュールにまかせたうえで、そのループ上で実行する各種のイベントを、イベントモジュールに依存しない形で書けるようにする、というのが本来の用途です。

もっと具体的にいうと、TkやGtk2を使ったGUIアプリケーションの裏で、Coroという、レーマン氏が2001年に(当時ActiveState社とMicrosoft社がおもにWindows環境でforkをエミュレートするために開発していたithreadに対抗するかのように)書いたコルーチン(軽量スレッド)モジュールを使って非同期処理を簡単に行うためのインタフェースといってもよいでしょう。

2005年の初公開当時、AnyEventはEvent、Coro、Tk、Glibにしか対応していませんでした。Pure Perlで書かれた自前のメインループを持つようになったのは2006年秋のバージョン2.0から。2007年秋にC/XSでEVというイベントループを自分で実装したあと(バージョン2.55⁠⁠、ユーザから問い合わせのあったWxをサポートするための手段としてか、あるいは別の思惑があったのか、すでにWxに対応済みだったPOEへの対応を本格化させたのは2008年4月リリースの3.3以降のことです。

知られざる存在からの脱却

このように、当初はむしろGUIアプリケーション向けの特殊なモジュールとみなされていたためか、これまでAnyEventには独自のコミュニティといえるようなものは存在していませんでした。メーリングリストに投稿はなく(以前はほかのイベントモジュールともどもperl-loopというメーリングリストを共用していたようですが、ここもほとんど無名の存在だったようです⁠⁠、レーマン氏のサイトからアクセスできる公式IRCチャンネルも、常駐している人は10名いるかいないかという程度。先日日本の有志がirc.perl.org上にAnyEvent用のチャンネルを開設しましたが、本稿執筆時点で同氏の参加はなく、不満や要望がある場合は直接本人宛にメールを送るか(RT経由でチケットを登録すると怒られます⁠⁠、同氏が参加しているIRCチャンネルを探して話しかけるくらいしかないのが現状です。

その意味で、AnyEventはまだ「コミュニティに認められてこその『モダン⁠⁠」という条件を満たしているとは言いがたいのですが、2008年春のPOE対応以降、AnyEventは各種イベントモジュールのアダプタとしてというより、POEよりもシンプルで高速なイベントモジュールとしてその存在感を高めてきているようです。

2008年6月にポルトガルで開催されたワークショップではAnyEventのベンチマーク結果を紹介する発表があったほか、最近では2009年2月にフランクフルトで開催されたワークショップや、2009年5月にモスクワで開催されたYAPC::Russia2009年6月にピッツバーグで開催されたYAPC 10、そして2009年9月に開催されるYAPC::AsiaでもAnyEvent関連のトークが予定されています。

そこで、ここではひとまずGUIアプリケーションのことは忘れて、AnyEventを(付属のPure Perlのメインループとともに)独立したイベントモジュールとして使う例と、メインループにEV、追加の非同期処理にCoro、そして両者をつなぐ架け橋としてAnyEventを使う例をそれぞれ簡単に紹介してみます[2]⁠。

AnyEvent単体でのイベントプログラミング

AnyEventのもっとも単純な使い方として、まずは標準入力を監視するだけのスクリプトを書いてみます。Windows環境でも動かしやすいよう、ここではあえてPure Perlのアダプタを使うように明示的に宣言しています。

#!perl
use strict;
use warnings;
use AnyEvent::Impl::Perl;
use AnyEvent;

my $cv = AnyEvent->condvar;
my $io; $io = AnyEvent->io(
    fh   => \*STDIN,
    poll => 'r',
    cb   => sub {
        chomp(my $input = <STDIN>);
        undef $io;
        $cv->send($input);
    },
);

if (defined(my $input = $cv->recv)) {
    print "got: [$input]\n";
}

いささか見慣れない表記もありますが、まず、$cvで受けているのは、ほかのライブラリではstateなどとも呼ばれる状態変数(オブジェクト)です。AnyEventではイベントを監視するWatcherでコールバック関数が実行された場合、この$cv(condvar)を通じてメインループにその旨を伝えることになっています。

次の$io = AnyEvent->io(...) の部分がWatcherを用意している部分。my $io = AnyEventy->io(...) と書かず、あえて my $io; $io = AnyEvent->io(...) とふたつに分けて書いているのは、コールバックのなかでWatcherをundefしたいから。詳しくはperldoc perlsubでmy () によるプライベート変数の話題を扱っている箇所をご覧ください。

ここではファイルハンドル(STDIN)の読み込みを監視して、値が入ってきたらcbで指定されているコールバック関数を呼び出し、そのなかでSTDINからデータを受け取ったあと、使い終わったWatcherを無効にして(これ以上イベントが呼び出されないようにして⁠⁠、状態変数を通じて受け取った値を外部に返す、という形になっています。

タイマーを追加してみよう

もちろんこれだけではただ my $input = <STDIN>; するのと変わりません。このスクリプトにもうひとつタイマーを追加してみましょう。Windows環境では意図したのとは異なる動作になってしまうかもしれませんが、これで標準入力に値を入れるまでタイマーの時刻が表示され続けるようになります。

@@ -15,6 +15,18 @@
     },
 );
 
+my $cv_timer = AnyEvent->condvar;
+my $timer; $timer = AnyEvent->timer(
+    after    => 0,
+    interval => 1,
+    cb       => sub {
+        print AnyEvent->time, "\n";
+        $cv_timer->send;
+    },
+);
+$cv_timer->recv;
+
 if (defined(my $input = $cv->recv)) {
     print "got: [$input]\n";
 }
+undef $timer;

イベントループの種類を変えてみよう

AnyEventの(本来の)キモは、イベントループの種類によらずこのスクリプトを実行できることです。いまは移植性を優先してPure Perlのループを使いましたが、今度は use AnyEvent::Impl::Perl; の行をコメントアウトしてみてください。AnyEventは(あれば)EV、あるいはEventを使ってループを処理してくれます(どちらも入っていなければ、結局Pure Perlのループになります⁠⁠。

EVやEventでの処理を確認できたら、POEでも動くか試してみましょう。これも例によってOSの種類やPOEのバージョン、AnyEventのバージョンなどによってエラーが出る可能性がありますが、AnyEventの特徴のひとつはつかめるのではないかと思います。

@@ -1,8 +1,9 @@
 #!perl
 use strict;
 use warnings;
-use AnyEvent::Impl::Perl;
+#use AnyEvent::Impl::Perl;
 use AnyEvent;
+use POE;
 
 my $cv = AnyEvent->condvar;
 my $io; $io = AnyEvent->io(

CoroとAnyEvent、EVを使った競馬ゲーム

さて、今度はCoroを加えて、簡単な競馬ゲームを実装してみます。async {}; の中身がCoroによるコルーチンで、その裏では(暗黙のうちにAnyEventが呼び出している)EVのループが回っています。コルーチンからメインループに処理を戻すときには cede; を、コルーチンの終了時には(AnyEventのイベントを止めるために)$cv->send を呼んで結果を順位保持用の配列に渡しているほか、念のためCoro::Handleを使って、イベントが回っている間の出力はブロックさせないようにしてみました(もちろんこの文脈なら単にprintするだけでも十分ですが⁠⁠。

#!perl
use strict;
use warnings;
use EV;
use Coro;
use Coro::AnyEvent;
use Coro::Handle;

my @cvs;
my $out = unblock \*STDOUT;
my $place = 0;
foreach my $i (1..10) {
    $cvs[$i] = AnyEvent->condvar;
    async {
        my $count = 0;
        until ($count > 10) {
            Coro::AnyEvent::sleep rand(1);
            $out->syswrite("ponie $i => $count\n");
            $count++;
            cede;
        };
        $cvs[$i]->send(++$place);
    };
}

my @places;
foreach my $i (1..10) {
    $places[ $cvs[$i]->recv ] = $i;
}

print "----------------\n";
foreach my $i (1..10) {
    print "$i => ponie $places[$i]\n";
}

今回のサンプルでは単に適当な時間、処理待ちをしているだけですが、もちろん実際のアプリケーションなら、コルーチンのなかにはウェブページを取ってくるコードなどが入ることでしょう。

今回は最初からCoro+AnyEvent前提でコードを用意しましたが、既存のアプリケーションでも、イベントループを呼ぶ前にCoroとCoro::AnyEventをロードして、非同期化したいところを async {}; ないし async_pool {}; でくくり、適当なところに cede; を挟んでやれば、基本的なCoro+AnyEventへの移行はおしまい。もちろん実際にはそれ以外にもさまざまな点で微調整が必要になってきますが、アプリケーションを全面的にイベント対応させることを思えば、このCoro+AnyEvent(+EV)の組み合わせは非常にコストパフォーマンスの高い改良になる「かも」しれません。

AnyEventは成功するのか?

Any系のモジュールが成功するかどうかはふつう、ほかの関連モジュールの開発者をいかにうまく取り込めるかにかかっています。

いかに理念がすばらしいものであっても、ほかの開発者に協力してもらえなければ、結局人は自分の慣れたイベントモジュールを使い続けるでしょうし、そのモジュール専用の拡張モジュールを書き続けてしまうものです。

ましてこの分野ではPOEという巨人がいて、たいていのことはすでにPOEの拡張モジュールが用意されています。AnyEvent用に調整したモジュールをリリースすることは単なる車輪の再発明にしかならない可能性も少なくありません。

そして、残念ながら「どんなイベントモジュールが相手でも使える関連モジュールを用意する」という文脈でのAnyEventの将来性は、決して明るいとはいえなさそうです。

前述の通り、いまの時点でAnyEventを積極的にサポートしていこうというコミュニティはほとんど見あたりません。AnyEventのドキュメント(とりわけAnyEvent::Impl::以下)には既存のモジュールやコミュニティに対する不平不満が大量に見られますから、既存のコミュニティからの建設的な協力を期待する方が野暮でしょう。とりわけPOEコミュニティとはAnyEvent::Impl::POEの実装やその結果生じたベンチマークの結果をめぐって鋭く対立してきた経緯があり、当面、両者が協力して事態の解決にあたることにはなりそうにありません[3]⁠。

しかも、最近はAnyEventに限らずAny系のモジュール全般に対して、いくら表面的なところを取り繕ってみたところで、環境(とりわけロードされているモジュール)によって動作が変わったり不安定になるようでは安心して使えない(それよりはそれぞれのバックエンドにあわせたモジュールを使うほうがいい⁠⁠、という批判が強くなってきているため、既存のイベント関連モジュールの作者がAnyEventに移行する理由はなくなってきています。

もっとも、既存のイベント駆動アプリケーションを読みやすくするためにコルーチンを追加したい、とか、ほかのイベントモジュールとの連携はおまけと考えて、Coro+AnyEvent+EVというレーマンウェアだけあればいい、という人がどのくらい増えるかはまた別の話ですし、こちらについてはまだまだ判断できるような状態にはありません。

HTTP::EngineRemedieのようにAnyEventへの対応を始めた国産モジュール/アプリケーションが今後どのような判断を下していくのかを含めて、日本での実験は海外からも注目されているようです。

おすすめ記事

記事・ニュース一覧