update: そういわれればこの例だけだったらgoto \&SUBでよかった!
例えば、AnyEvent::DBIで Q4Mを使って、*常に*なんらかのイベントをqueue_wait()する状態にしたいとする。するとexec()が終わったらまた同じexec()を呼ぶ事になるので、例えばこんなコードを書くかもしれない。
use strict; use AnyEvent::DBI; my $sql = "SELECT .... FROM queue_table WHERE queue_wait('queue_table', 10)"; my $dispatch; $dispatch = sub { $dbh->exec( $sql, sub { # 返ってきた値でなんかする $dispatch->(); }); }; $dispatch->(); # まぁ他の事してるプログラムで使うだろうから、本来はいらないけど・・・ AE::cv->recv;
でもこれだと再帰的に exec()を呼んで、さらにまた$dispatchからexecを呼んで・・・って事になってしまうよね。これを無くしたい。検証のためにもっとも単純なクロージャを使った再帰呼び出しをまず書いてみる。ここではスタックの深さをついでに表示しておく:
use strict; my $dispatch; $dispatch = sub { my $i = 0; while(1) { my @caller = caller($i++); # noop last if ! scalar @caller; } print "$i\n"; $dispatch->(); }; $dispatch->();
このコードを実行すると、エラーを起こすまで、少しずつ表示される数が大きくなっていく。これは再帰的に関数を呼び出し続けている数なわけで、どこかの時点でdeep recursion云々言われる。当たり前だよね。
さて、ではこれをAnyEventを使ってる場合はどうするか。まずひとつ簡単な方法としては idleウォッチャーを設定する事ができる:
use strict; use AnyEvent; my $idle; my $dispatch; $dispatch = sub { my $i = 0; while(1) { my @caller = caller($i++); # noop last if ! scalar @caller; } print "$i\n"; $idle = AE::idle $dispatch; }; $idle = AE::idle $dispatch; AE::cv->recv;
このコードを実行した場合、自分の環境では"6"が繰り返し表示されるだけ。idle()はイベントループが「次にビジーでない状態」に与えられた関数を実行するウォッチャーなので、続々と実行してくれるわけです。
「次にビジーでない状態」という曖昧な状態ではなく、確実にすぐ実行したい場合はタイマーを使ってもよいかも。この場合最初の引数に0を渡しておいてやれば、とにかく次の機会にすぐクロージャを実行してくれる。この場合も自分の環境では"6"が繰り返し表示される:
use strict; use AnyEvent; my $count = 1; my $w; my $dispatch; $dispatch = sub { my $i = 0; while(1) { my @caller = caller($i++); # noop last if ! scalar @caller; } print "$i\n"; $w = AE::timer 0, 0, $dispatch; }; $w = AE::timer 0, 0, $dispatch; AE::cv->recv;
というわけで、直接Perlの関数を呼び出すのではなく、イベントループに一旦預ける事によって再帰的呼び出しを続けるのを減らすことができるのです。
コメント