24
23

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

AnyEvent のタイマーを自由自在に操る

Last updated at Posted at 2015-07-09

AnyEvent とは

AnyEvent とは、Perl のイベント駆動フレームワークです。ウェブ開発エンジニアにも JavaScript などですっかりおなじみになったイベント駆動を包括的に扱うことができます。

詳しくは別の資料や記事に譲りますが、AnyEvent では標準で以下の様なイベントに対応しています。

  • 時間を基準としたタイマー
  • I/O 発生によるフック
  • 子プロセスの終了
  • 非同期ネットワークI/O
  • シグナルハンドラ

これらを使って、以下の様な応用モジュールが CPAN でたくさん公開されています。

  • 非同期HTTPクライアント
  • IRCなどのチャット系ボット作成支援モジュール
  • ファイル更新通知
  • 非同期版DBI

このあたりについても詳しく書きたいのですが、AnyEvent 自体の歴史も長いことで(それがゆえに今では古い情報がたくさん残るという状況もあるのですが)資料が多いので、まずは検索して探していただくのがいいかと思います。

この資料では、AnyEvent のタイマー部分についての解説をします。

AnyEvent の基礎の基礎

AnyEvent が初耳の Perl プログラマーの方にざっと概要を説明します。

AnyEvent では状態変数 (Condition Variable) というものを中心に、イベント駆動の統括を行います。この状態変数を取得するのが AnyEvent->condvar メソッドです。このメソッドは見て分かる通りファクトリメソッドです。

use AnyEvent;
my $cv = AnyEvent->condvar;

短縮記法では AE::cv と書かれますが、同じことです。この記事では短縮記法については省略します。

次に、各種イベントを定義します。ここでは時間によるイベントを司る timer メソッドのみ扱います。

use AnyEvent;
my $cv = AnyEvent->condvar;

$| = 1;
my $timer1 = AnyEvent->timer(
  after => 1,
  interval => 2,
  cb => sub { print "."; }
);

引数を見てなんとなくわかりますが、1秒後から2秒おきに、実行したコンソールの画面上にドット記号を出していくというものです。改行文字を入れず連続して print をすると出力バッファにためられてしまうので、$| = 1 しています。$| の詳細は perlvar を参照ください。

変数 $timer1 に格納しましたが、このままでは実行されません。

実際にイベントループを回してイベントを開始させるには $cv->recv() を実行する必要があります。

use AnyEvent;
my $cv = AnyEvent->condvar;

$| = 1;
my $timer1 = AnyEvent->timer(
  after => 1,
  interval => 2,
  cb => sub { print "."; }
);
$cv->recv(); # ここでイベントループがまわる

キリの良い時間、たとえば毎時0分になったらイベントループ全体を止めてみましょう。毎時0分の場合、time 関数で得られるエポック秒を 3600 で割った値は 60 未満になっているはずです。

use AnyEvent;
my $cv = AnyEvent->condvar;

$| = 1;
my $timer1 = AnyEvent->timer(
  after => 1,
  interval => 2,
  cb => sub {
  	print ".";
  	if ( time % 3600 < 60 ) {
  	  $cv->send();
  	}
  }
);
$cv->recv(); # ここでループがまわる

イベントループ全体を止めるには、状態変数の send メソッドを呼ぶと良いです。sendrecv すると覚えるとよいです。

AnyEvent のコードでは、なにかエラーや例外が発生したらその情報を send の引数に入れるというのがおなじみの書き方です。その情報は recv の返り値で取得できます。特に問題がなければ、引数なしで send を呼べばよいのです。

use AnyEvent;
my $cv = AnyEvent->condvar;

$| = 1;
my $timer1 = AnyEvent->timer(
  after => 1,
  interval => 2,
  cb => sub {
  	print ".";
  	if ( time % 3600 < 60 ) {
  	  $cv->send("毎時0分はなぜかマズイ!");
  	}
  }
);

my $error = $cv->recv(); # ここでループがまわる

if ( $error ) {
   print "ERROR: $error\n";
}

コードにするとこんな感じでしょうか。

今は一つしかイベントを扱っていないので、while ループにはないメリットが見えづらいです。とはいえ、多くのイベントを平行して扱う場合にこの AnyEvent は非常に強力なものとなります。

$| = 1;
sleep 1;
while(1) {
  print ".";
  if ( time % 3600 ) {
    last;
  }
  sleep 2;
}

イベント一つならこれで十分とはいえ、最後の sleep 2 を入れ忘れると大暴走となったり、もう一つイベントを追加しようと思った途端に複雑になったりするところが問題点です。

実際に $cv->recv() の部分で while(1){ ... } ループが回っていて、AnyEvent はそれを扱いやすくするために高度な抽象化をしてくれると考えても差し支えないでしょう。

$timer1 は代入して一度も使っていませんでした。以下でそれについて解説します。

タイマーをキャンセルする簡単な方法

代入で確保したのに使わなかったオブジェクトですが、これをそのまま消す、つまり参照されなくなってガベージコレクタで消去されたとき、そのタイマー(イベント)は停止します。

3つのタイマーを作ってみます。

  • 1つ目のタイマーは3秒おきに "x" を出力する
  • 2つ目のタイマーは5秒おきに "y" を出力する
  • 3つ目のタイマーは20秒後に1つ目のタイマーをキャンセルし、30秒後に2つ目のタイマーをキャンセルし、40秒後にループ全体を正常終了させる

コードは以下です。

#!/usr/bin/env Perl

use strict;
use warnings;

use AnyEvent;

my $cv = AnyEvent->condvar;

$| = 1;

my $timer1 = AnyEvent->timer(
    interval => 3,
    cb       => sub { print "x"; },
);
my $timer2 = AnyEvent->timer(
    interval => 5,
    cb       => sub { print "y"; },
);
my $timer3_past = 10;
my $timer3 = AnyEvent->timer(
    after    => 10,
    interval => 10,
    cb       => sub {
        print "[timer3:$timer3_past]";
        if ( $timer3_past == 20 ) {
            undef $timer1;
        } elsif ( $timer3_past == 30 ) {
            undef $timer2;
        } elsif ( $timer3_past == 40 ) {
            $cv->send();
        }
        $timer3_past += 10;
    },
);

$cv->recv();

print "[end]\n";

実際に実行してみて、print をはさんだりするとよくわかります。手元の実行環境では結果は以下のようになりました。

$ ./ae.pl
xyxyxxy[timer3:10]xxyx[timer3:20]yyy[timer3:30][timer3:40][end]

$cv->recv() でループに入ったあともタイマーを作成することができます。この場合は作成した途端にイベントが発生します。

#!/usr/bin/env Perl

use strict;
use warnings;

use AnyEvent;

my $cv = AnyEvent->condvar;

$| = 1;

my $char = "a"; # $char++ => "b"

my @char_timers;

my $timer1 = AnyEvent->timer(
    interval => 1,
    cb       => sub {
        print ".";
        my $output_char = $char++;
        push @char_timers, AnyEvent->timer(
            interval => 1,
            cb       => sub { print $output_char; },
        );
    },
);
my $timer2 = AnyEvent->timer(
    after => 10,
    cb    => sub { $cv->send(); },
);

$cv->recv();

print "[end]\n";

秒数が経つごとに同時に挿入されるアルファベット1文字が多くなっていくというものです。別のタイマーが10秒後にループ自体を止めています。

これの実行結果は手元では以下のようになりました。

$ ./ae1.pl
.a.ab.acb.adcb.adcbe.adcbef.agdcbef.hagdcbef.haigdcbef.haigjdcbef.haigjdc

同じタイミングで文字を入力しようとすると、タイマーを作ったアルファベット順にはならないというのが面白いですね。このあたりの原理は私も深くは調べていません。

このように、タイマーは after interval そして cb と 3つの情報を持つことがわかりました。ただ、状況に応じてこの interval 値を変えたり、さらには(多くの場合は作りなおしたほうが早いですが)タイマーのイベントコールバックを差し替えることは可能なのでしょうか。

タイマーを自由自在に活用する

今までは、タイマーのキャンセルのためにしか使っていなかったオブジェクトですが、この正体はなんなのでしょうか。簡単な方法で調べてみましょう。

$ perl -MAnyEvent -E 'say ref AnyEvent->timer(after=>1,cb=>sub{});'
EV::Timer

AnyEvent->timer() で生成されたオブジェクトは EV::Timer というクラスによるもののようです(AnyEventの呼び出し方など、状況によっては違うかもしれません)。

EVのドキュメントを読んでみると、例えば set メソッドでタイマーの設定情報を変えることができるようです。

#!/usr/bin/env Perl

use strict;
use warnings;

use AnyEvent;

my $cv = AnyEvent->condvar;

$| = 1;

my $timer1 = AnyEvent->timer(
    interval => 1,
    cb       => sub {
        print ".";
    },
);
my $timer2 = AnyEvent->timer(
    after => 10,
    cb    => sub {
        # after => 0, interval => 3
        $timer1->set(0, 3);
    },
);

$cv->recv();

print "[end]\n";

実際に実行してみると、10秒後になって "." が表示されるのが1秒おきから3秒おきになったことがわかります。

EV のドキュメントを読むと、その他にいろいろなことができそうです。

ブロッキング処理に関する注意

ここまでタイマーの話を扱ってきましたが、一つ注意点があります。それは コールバックで実行する処理はイベント全体をブロックしてはいけません

ブロックするというのは、例えば時間のかかるネットワークI/Oだったりといったものです。一番明白なブロッキング命令は sleep なのですが、それは例に出すまでもないでしょう。

たとえば、前述の1秒ごとに "." を出力するプログラムで、開始10秒後に LWP::UserAgent で HTTP アクセスをしてみましょう。

#!/usr/bin/env Perl

use strict;
use warnings;

use AnyEvent;
use LWP::UserAgent;

my $cv = AnyEvent->condvar;

$| = 1;

my $timer1 = AnyEvent->timer(
    interval => 1,
    cb       => sub {
        print ".";
    },
);
my $timer2 = AnyEvent->timer(
    after => 10,
    cb    => sub {
        my $ua = LWP::UserAgent->new();
        for my $host (qw(www.yahoo.co.jp www.yahoo.com www.yahoo.co.uk)) {
            my $res = $ua->get("http://$host/");
        }
    },
);

$cv->recv();

print "[end]\n";

アクセスをするだけでレスポンス結果は一切使っていませんが、このプログラムを実行したら、10個か11個の "." が出力されたところでしばらく出力が停止すると思います。これは LWP::UserAgent の裏側のネットワーク I/O がブロッキング I/O であることに起因します。

このようなイベントブロッキングが発生しないようにするには、HTTP アクセスであればノンブロッキング版のモジュールを使うことです。たとえば AnyEvent には AnyEvent::HTTP というモジュールがあり、それを使うとよいです。

#!/usr/bin/env Perl

use strict;
use warnings;

use AnyEvent;
use AnyEvent::HTTP;

my $cv = AnyEvent->condvar;

$| = 1;

my $timer1 = AnyEvent->timer(
    interval => 1,
    cb       => sub {
        print ".";
    },
);
my $timer2 = AnyEvent->timer(
    after => 10,
    cb    => sub {
        http_get "http://www.yahoo.co.jp/", sub {
            my ($data, $headers) = @_;
            printf "[%d]", length $data;
            http_get "http://www.yahoo.com/", sub {
                my ($data, $headers) = @_;
                printf "[%d]", length $data;
                http_get "http://www.yahoo.co.uk/", sub {
                    my ($data, $headers) = @_;
                    printf "[%d]", length $data;
                };
            };
        };
    },
);

$cv->recv();

print "[end]\n";

実行結果は手元では以下のようになりました。

............[23319]..[354467]...[9000]...^C

途中で Ctrl-C で終了させています。"." の出力は前回のプログラムとは違って滞ったりはしませんでした。

パッとみて「これがコールバック地獄か…」となりますが、これを避けるためには自分で気の利いたサブルーチンを書くのも一つですし、あとは CPAN にある AnyEvent::Promise などのモジュールを試してみるのもよいでしょう。(Coro という AnyEvent とは別の選択肢についてはここでは触れません)

AnyEvent::HTTP などが使っているノンブロッキングネットワークI/Oの内側は結構難しかったりするので、興味のある方は中を見てみると面白いかもしれません。核心は AnyEvent::Socket で、そこでは Perl コアモジュールである Socket や標準関数の socket などが複雑に使われています。

だいたい Perl でお馴染みのネットワーク系モジュールは、ノンブロッキングであることを明示していなければ大抵はブロッキングI/Oだといえます。もちろん、ブロッキングしても全体のイベントループに与える影響が無視できると断言できる場合であれば問題ありませんが、様々な状況を想定しておいたほうがよいでしょう。DNS の問い合わせが長時間返ってこないといった状況は普通にありえます。

最後に

ここまでわかると、AnyEvent のタイマーでだいぶ思い通りのことができる気がします。他に AnyEvent が扱えるイベントもこの流れでマスターしていくことは難しくないでしょう。

AnyEvent の詳しい話は、リクエストがあればどこか別のところに書くことにします。

24
23
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
23

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?