プログラムを書く際に処理をタイムアウトさせたい場合というのはよくあると思います。
そんなときに便利なのが、alarmです。
今回は、Perlプログラムでalarmを使ってタイムアウト処理を実装したいと思います。
alarmは対象の処理が指定した時間(秒)たっても終了しない場合、SIGALRMシグナルを発生させます。
例として、ユーザからの入力を30秒でタイムアウトさせるプログラムを書いてみます。
#!/usr/bin/perl
use strict;
# タイムアウトするまでの時間(秒数)
my $TIMEOUT = 30;
print "What your name??: ";
alarm($TIMEOUT);
my $name = ;
my $timeleft = alarm(0);
print "Hello! $name";
alarmで囲った部分、ユーザからの入力待つ部分がタイムアウト処理の対象になります。
ユーザが30秒以上入力しない場合、SIGALRMシグナルが発生しプログラムが終了します。
実際に30秒放置しておくと以下のような画面になります。
[hiroki@capybara]~% ./sig_alarm.pl
What your name??: zsh: alarm ./sig_alarm.pl
[hiroki@capybara]~%
このままでもプログラムを終了させることはできるのですが、SIGALRM発生後に後処理を行いたいということが多いと思います。
# このプログラムでは特にこのままでも問題ないのですが...
上記のプログラムを改良しタイムアウトした際にメッセージを表示するようにしたいと思います。
evalとdieを組み合わせることで後処理を実装することができます。
下記が実装例です。
#!/usr/bin/perl
use strict;
my $TIMEOUT = 30;
print "What your name??: ";
my $name = '';
eval {
local $SIG{ALRM} = sub {die};
alarm($TIMEOUT);
$name = ;
my $timeleft = alarm(0);
};
if ($@) {
# タイムアウト
print "\nERROR: TIMEOUT\n";
}else{
# 正常終了
print "Hello! $name";
}
まず、タイムアウト処理をしたい対象をevalで囲んでdieしたときでもプログラム自体を終了しないようにします。
dieなどの割り込みが発生した場合には、$@にエラーメッセージが代入されるのでそれによって処理がタイムアウトしたかを判定しています。
eval内では、SIGALRMシグナルが発生したときでもプログラムが終了しないようにSIGALRMのシグナルハンドラを設定しています。
シグナルハンドラには関数のリファレンスを渡す必要があります。
上記例では、SIGALRMが発生した際にはdieするようにしています。
シグナルハンドラはlocalをつけて指定することで、スコープ内で発生した該当のシグナルだけつかまえることができます。
localを指定していない場合、プログラム全体の該当のシグナルハンドルを指定することになります。
上記のプログラムを実際にタイムアウトさせると下記のようになります。
[hiroki@capybara]~% ./sig_alarm.pl
What your name??:
ERROR: TIMEOUT
[hiroki@capybara]~%
今度はシェルからのメッセージではなく、きちんと自分自身のプログラムからのメッセージが表示されているのがわかると思います。
最後にalarmとsleepを併用することは推奨されません。
sleepがalarmを使って実装されていることが多く、正しく動かないことがあるからです。
alarmとevalとdieを組み合わせることでタイムアウトがないような処理にもタイムアウトが実装できるようになります。