Perl Hackers Hub

第2回AnyEventでイベント駆動プログラミング (2)

ウォッチャー

AnyEventでプログラムを作成する場合「ウォッチャー」を作成、管理することが基本的な作業となります。ウォッチャーとはI/Oやタイマーなどの何かしらのイベントが発生したことを通知してもらうためのオブジェクトです図2のコールバックの指定および実行の部分を担当します⁠⁠。

現在から5秒後にコールバック関数を呼び出してもらうにはリスト1のようなコードを書きます。

リスト1 ウォッチャー
use strict;
use AnyEvent;

my $cv = AnyEvent->condvar; ……(1)

# タイマーウォッチャーを作成
my $w; $w = AnyEvent->timer(
    after => 5, # 今から5秒経ったらイベント発生
    cb => sub { # イベント発生時にこの関数が呼ばれる
        warn "5秒経ちました!";
        undef $w; ……(2)
        $cv->send;
    }
);

$cv->recv; ……(3)

リスト1(1)のように$wに入れた変数をコールバック関数の中で明示的に解放しているのは、コールバック関数が実行されるまで$wの中身をメモリ解放せずに有効にしておきたいからです。AnyEvent->timer関数によって作成されたウォッチャーオブジェクトはメモリ解放されてしまうとイベントが発生しなくなってしまうので、このように明示的に解放するタイミングをコントロールしてあげる必要があるのです。

リスト1(1)で生成している$cvはコンディション変数Condition Variableと呼ばれ、これも一種のイベント通知のために使われます。リスト1の場合、(3)で$cv->recvが呼ばれた時点でイベントループにコントロールが渡り、$cv->sendが呼ばれるまでこのあとのコードが実行されることはありません。つまり$wに指定したコールバック関数が実行されるまで待つ、といった状態になります。コンディション変数にはもっとほかの使い方もありますが、それについては後述します。

AnyEventにはこのタイマーのほかにもいくつかのウォッチャーオブジェクトが存在します。書き出してみるとその種類が意外と少ないことに驚かれるかもしれませんが、実際にコードを書いてみるとこれだけでほとんどのことができることがわかります。

それではどのようなウォッチャーがあるのか見てみましょう。

タイマーウォッチャー

書式
AnyEvent->timer(
    after => $seconds,
    interval => $seconds,
    cb => $cb

);

まずは先述したタイマーウォッチャーです。このウォッチャーは任意のタイミングでイベントを発生させます。リスト2の場合、10秒後にイベントを発生させ、警告を出力してから終了します。

リスト2 タイマーウォッチャー
my $w; $w = AnyEvent->timer(
    after => 10,
    cb => sub {
        warn "10秒経ったよ!";
        undef $w;
    }
);

タイマーは任意のタイミングだけでなく、定期的にイベントを発生させることもできます。リスト3ではまずウォッチャーを作成し、(1)で指定したとおり10秒後にイベントを発生させます。最初のイベントが起こると、そのあと(2)で指定したように1秒ごとに再度イベントが発生します。

リスト3 一定間隔でタイマーを発動
my $count = 0;
    my $w; $w = AnyEvent->timer(
    after => 10, ……(1)
    interval => 1, ……(2)
    cb => sub {
        $count++;
        warn "タイマー発動! ($count回目)";
        if ($count >= 10) {
            undef $w;
        }
    }
);

タイマーを使用する際に重要なのは、タイマーは正確にN秒後(またはN秒おき)にイベントを発生させるのではなく、最低N秒経ったあとにイベントが発生するということです。ほとんどの場合これは誤差の範囲ですが、イベント駆動プログラミングの場合は正確な時間にイベントが発生される保証はないことを念頭に置いてタイマーを使用する必要があります。

I/Oウォッチャー

書式
AnyEvent->io(fh => $fh, poll => $mode, cb => $cb);

I/OウォッチャーはI/Oイベントを監視します。任意のハンドルが書き込み/読み込み可能になったかどうかの判断を行ってくれます。

fhには監視するファイルハンドルを指定します。pollにはそのファイルハンドルに対して読み込み/書き込みイベントのどちらを監視するのかを"r"(読み込み)"w"(書き込み)で指定します。cbはイベント発生時に呼ばれるコールバック関数を指定します。コールバックは引数を受け取りません(バックエンドによっては引数を渡される場合がありますが、仕様には定義されていないのでこれに依存してはいけません⁠⁠。

リスト4の場合、(1)で指定した$fhが書き込み可能(2)"w"を指定したため)になりしだいコールバック関数を呼び出し、文字列を書き込むというウォッチャーを作成しています。

リスト4 I/Oウォッチャー
my $fh = ....; # ソケットなど

my $io; $io = AnyEvent->io(
    fh => $fh, ……(1)
    poll => "w", ……(2)
    cb => sub {
        # 実際に使う際は、syswriteのエラーなどを
        # もっと確認してください
        syswrite( $fh, "hoge hoge hoge" );
        undef $io;
    }
);

これを使ってソケットなどから読み込み可能になったら読み込めるだけ読み込むようなコードを書く場合はリスト5のようにします。

リスト5 I/Oウォッチャーでソケットからデータを読み込む
my $io; $io = AnyEvent->io(
    fh => $fh,
    poll => "r",
    cb => sub {
        my $len = sysread( $fh, my $buf, 8192 );
        if ($len > 0) {
            print "read '$buf'\n";
        } elsif (defined $len) {
            # ハンドルがcloseされた
            undef $io;
        } elsif ( $! != EAGAIN && $! != EINTR ) {
            # よくないエラー
            undef $io;
            die "An error occurred: $!";
        }
    }
);

このしくみを利用して複数のホストに同時接続しながらそれぞれの接続が読み込み可能になったところからコンテンツをダウンロードするということもできますが、それについてはもっと便利なライブラリAnyEvent::HTTPがあるので後述します。

アイドルウォッチャー

書式
AnyEvent->idle(cb => $cb)

イベントループが回っていても、時折そのターンでは何もすることがない、という状況があります。読み込みを待っているソケットがどれもまだ読み込み可能になっていない、設定したタイマーがまだ発生する時間になっていない……などの状況が考えられます。アイドルウォッチャーはそんな「何もすることがない」タイミングでイベントを発生させます。

このウォッチャーは何か処理を行う必要があるものの、⁠次にできるタイミング」でできればよい(つまりタイミングが重要ではない)処理をするときに重宝します。リスト6の場合はタイマーがすぐにafter => 0イベントを発生させるので最初はアイドルウォッチャーは発動しませんが、その後1秒何もイベントが存在しない間隔があるので、その間にアイドルウォッチャーで定義したイベントが発生します。

リスト6 アイドルウォッチャー
my $cv = AnyEvent->condvar;
my $count = 1;
my $t; $t = AnyEvent->timer(
    after => 0,
    interval => 1,
    cb => sub {
        warn "timer!";
        if ($count++ > 10) {
            undef $t; ……(1)
            $cv->send;
        }
    }
);

my $w; $w = AnyEvent->idle(
    cb => sub {
        warn "idle";
        undef $w;
    }
);

$cv->recv;

なお、このウォッチャーは1回イベントを発生させたあとも、(1)のように明示的に無効にしない限り繰り返し呼び出されるので注意してください。

シグナルウォッチャー

書式
AnyEvent->signal( signal => $name, cb => $cb )

シグナルウォッチャーは、なんらかのシグナルを受け取ったときにイベントを発生させます。⁠Ctrl][C]を押したときにクリーンアップを行いたいというような場合に指定するとよいでしょうリスト7⁠。

リスト7 シグナルウォッチャー
my $w; $w = AnyEvent->signal(
    signal => 'INT',
    cb => sub {
        # クリーンアップ処理
        undef $w;
    }
);

なお、シグナルウォッチャーとPerl組込みのシグナル処理(%SIG)を併用すると正しく動作しないことがあるので注意してください。

コンディション変数

先ほど少し紹介したコンディション変数は、複数の意味を持つ重要なコンセプトです。

特定の条件が満たされるまで待つ

一番わかりやすい使用方法はなんらかの条件が満たされるまで処理をそこでブロックするために使うことです。

まず簡単な例として、起動後10秒待って停止するプログラムを見てみましょうリスト8⁠。リスト8(1)でコンディション変数$cvを生成しています。

リスト8 コンディション変数
use strict;
use AnyEvent;

my $cv = AnyEvent->condvar; ……(1)
my $w; $w = AnyEvent->timer(
    after => 10,
    cb => sub {
        undef $w;
        $cv->send; ……(2)
    }
);

$cv->recv; ……(3)

このスクリプトを(3)の$cv->recvをコメントアウトして実行すると、タイマーが発動することなく終了するはずです。(3)がないと、メイン処理の実行を止めてイベントループが動作開始するための「待ち」がないからです。$cvを設定することによって待ちを入れることができます。

リスト8では、まずタイマーウォッチャーを定義し、その後(3)のrecvを指定することによって、いつか$cvにsendが呼び出されるまでブロックします。(3)でブロックするとイベントループが起動し、事前に定義したタイマーウォッチャーは10秒後に起動され、コールバックが実行されます。ただしブロックと言っても、システムコールのブロックとは違いイベントループに処理の主導権を渡すだけで、ほかのイベントの実行を妨げるという意味ではないことに注意してください。

タイマーウォッチャーが起動すると、まずウォッチャー自身を無効化したあと(2)でコンディション変数$cvに対してsendが呼び出されます。このイベントコールバックが終了したあと、$cv->recvでブロックされていた処理がリリースされ、その次の処理(この場合はスクリプトの終了)を実行します。

なお、リスト8のようにブロックするrecvの呼び方はコールバック内ではできません。コールバック内でブロックするとイベントループもブロックされてしまうからです(コールバック内で似た動作をしたい場合は、後述の「処理終了時のコールバックとして使う」を参照してください⁠⁠。

複数の条件が満たされるまで待つ

リスト8ではsendを1回呼べば条件が満たされましたが、複数回呼び出されるのを待つこともできます。リスト9では10個のタイマーが発動し終わるとスクリプトが終了するようになっています。

リスト9 複数条件を待つコンディション変数
use strict;
use AnyEvent;

my $cv = AnyEvent->condvar( cb => sub { ……(1)
    warn "executed all timers";
});

for my $i (1..10) {
    $cv->begin; ……(2)
    my $w; $w = AnyEvent->timer(after => $i, cb => sub {
        warn "finished timer $i";
        undef $w;
        $cv->end; ……(3)
    });
}

$cv->recv; ……(4)

(1)ではまずコンディション変数を作成しますが、その際無名関数を引数として渡しています。この関数は$cvの待ち条件が満たされたときに実行される関数です。つまり、(4)のrecvがブロックから解放されるタイミングで実行されます。

(2)ではタイマーウォッチャーを1つ作成するたびにbeginを呼び出しています。beginを呼ぶと$cvの内部カウンタが上昇し、beginの呼ばれた回数分だけendを呼び出すまで$cvの条件が満たされることはありません。

タイマーがそれぞれ発動するコールバックの中の(3)で$cvにendが呼び出されますので、10個目(最後)のタイマーが発動すると$cvを解放する条件が満たされて$cv->recvのブロックが解けるわけです。この際(1)で指定した関数も実行されますので、"executed all timers"というメッセージが表示され、スクリプトが終了します。

処理終了時のコールバックとして使う

最後の使い方は、暗示的に任意のコールバック関数を呼び出すためにコンディション変数を使う方法です。コンディション変数は作成時に(もしくはcb関数を使うことによって)条件が満たされたときに実行するコールバックを指定できます。

簡単な例はすでにリスト9で解説しましたが、リスト10では標準入力から1行だけ入力を読み込むread_stdin関数が自らコンディション変数を作成して戻り値として返すようにしてあります。

リスト10 コールバックの利用
use strict;
use AnyEvent;

AnyEvent::Util::fh_nonblocking(\*STDIN, 1);

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

my $main_cv = AnyEvent->condvar;

{ # 1個目。こちらはSTDERR にprint ……(1)
    my $cv = read_stdin();
    $cv->cb( sub {
        my ($line) = $_[0]->recv;
        print STDERR "STDERR: got $line";
        $main_cv->send;
    } );
}

# 1個目の読み込みが終わるまで待機……
$main_cv->recv;

$main_cv = AnyEvent->condvar;

{ # 2個目。こちらはSTDOUTにprint ……(2)
    my $cv = read_stdin();
    $cv->cb( sub {
        my ($line) = $_[0]->recv;
        print STDOUT "STDOUT: got $line";
        $main_cv->send;
    } );
}

# 2個目の読み込みが終わるまで待機……
$main_cv->recv;

read_stdin関数は1行読み込みを行ったあとどうすればよいかわからないので1行読み込めるまで待ち、読み込めたらその値を$cvにsendを使用して渡します。$cvはその時点ですでにread_stdinの呼び出し元に戻り値として戻っているので、呼び出し側がそれを使って処理を待つなり無視するなりの処理方法を決めることができるわけです。

(1)で標準エラーに、(2)で標準出力にread_stdinで取得した入力値を出力するようにしてあります。このようにコンディション変数を返す関数を作成することで、いつ終了するかわからない関数が終了したことを感知したり、その関数の結果を利用したりすることができます。

コンディション変数はこのほかにも、うまく組み合わせることでトランザクションを実装するために使用したりとさまざまな用途に使えますので、ぜひいろいろ試してみてください。

おすすめ記事

記事・ニュース一覧