処理に時間がかかりそうな処理を書く際、タイムアウト時間を設定して、それを超えても処理が完了しなければ、 エラーを表示するような処理が必要になることがあります。 特に、ソケットを使って外部サーバーと通信する場合などがあてはまります。 もし相手のサーバーからの応答が無かったらどうなるでしょうか。 応答があるまで待ち続けることになります。 または、ブラウザー側が見切りをつけて待機することをやめてしまうでしょう。 もしご利用のレンタルサーバーに CGI の処理時間に制限があれば、Internal Server Error となるでしょう。
ここでは、秒数を指定してタイムアウト時の処理ができるうようにする方法を解説します。
タイムアウトの処理を実装する方法として、ALRM シグナルを使う方法を解説します。
シグナルとは、イベントが発生した際に、オペレーティングシステムがプロセスに伝達する信号とお考え下さい。
このコーナーではタイムアウト処理を扱いますので、
タイムアウトしたという事実をイベントとしてプロセスに伝えてあげる必要があります。
そして、スクリプトには、そのタイムアウトしたというイベントをキャッチするようにしなければいけません。
これを実現するためには alarm
関数を使います。
シグナルにはさまざまな種類があるのですが、alarm
関数は、各種シグナルのうち、
ARLM
というシグナルを発生させるようシステムに要求することができます。
※ タイムアウト処理ではシグナルを使うため、Windows 系サーバーでは使えません。
スクリプトでは、ざっくりと以下の流れになります。
$SIG{ALRM} = sub {
# タイムアウトしたときの処理をここに書く
}
alarm 10; # タイマーを開始(ここでは 10 秒)
#タイマーで時間を監視したい処理
alarm 0; # タイマーを終了
順に処理内容を見ていきましょう。
$SIG{ALRM} = sub {...}
ALRM
シグナルをキャッチした際に実行する処理を定義します。
右辺には、サブルーチンのリファレンス、もしくは、無名サブルーチンを指定します。
ここでは無名サブルーチンを指定しています。
alarm 10
タイマーをセットします。この行からタイマーがスタートすることになります。
この行では、alarm
関数の引数として 10 を指定しております。
これは、10 秒後に ALRM
シグナルを発するようシステムに要求をしていることになります。
alarm 0
タイマーをキャンセルします。これは忘れないようにして下さい。
もし時間内に処理が完了したにも関わらずタイマーが動き続けていると、
その時間になったら ALRM
シグナルが発生し、
$SIG{ALRM}
にセットしたサブルーチンが実行されてしまいますので注意してください。
上記スクリプト例は、あくまでも流れを見ていただくためのものです。 これでもタイムアウト処理は実現できますが、完全ではありません。
タイムアウトした際に実行する処理(この場合、$SIG{ALRM}
にセットしたサブルーチンの処理)がただ単に、
メッセージを print
文で表示するだけの場合、スクリプトが終了しないことがあります。
例えば、こんなスクリプトをがんが得てみましょう。
$SIG{ALRM} = sub { print "timeout\n" };
alarm 3;
open(FILE, ">>./$file");
flock(FILE, 2);
alarm 0;
このスクリプトを実行すると flock
が完了するまでスクリプトが終了しません。
もちろん、3 秒後には timeout
とメッセージが出力されますが、その後、ずっと待ち続けてしまいます。
なぜなら Perl はシステムコールを再び試みようとするからなのです。
従って、ALRM
シグナルをキャッチした際には、exit
関数等を使って、
必ずスクリプトを終了するようにしなければいけなくなります。
しかしそうはいかない場合もあるでしょう。
上記問題をクリアするためには eval
関数を使います。
タイムアウトの処理をする場合には、以下の用法を使えばトラブルも少なくなるでしょう。
eval {
local $SIG{ALRM} = sub { die 'timeout' };
alarm 10;
#タイマーで時間を監視したい処理
alarm 0;
};
alarm 0;
if($@) {
if($@ =~ /^timeout/) {
# タイムアウト時の処理
} else {
# その他の例外処理
}
}
1 行目から 8 行目にかけて、一連のタイマー処理を eval
で囲みます。
7 行目の alarm 0
は忘れないように気をつけてください。
eval
の中で注目していただきたいのが 2 行目です。
シグナルハンドラとして無名関数(サブルーチン)を指定しておりますが、その中で、die
を使っていることに注目してください。
タイムアウトした場合、その中の処理を完全に取りやめて抜け出すために die
関数を使う必要があります。
しかし、もしあなたが CGI を作っているとしたら、ちょっと心配かもしれませんね。
そうです。CGI の場合、die
を使って処理を終了すると、当然のことながら、
Internal Server Error となってしまいますよね。
しかし、ご安心ください。ここは eval
の中なんです。
eval
内で die
を使うと、例外が発生したとみなされ、
その内容は $@ に格納され、スクリプトは終了しません。
die
で eval
を抜け出した後の処理は、12 行目からとなります。
10 行目にも alarm 0;
がありますが、これは必要です。
もし 7 行目の alarm 0;
に到達する前に時間切れになった場合、
どこかでタイマーをキャンンセルしなければいけないからです。
従って、eval
の処理を抜けた直後にタイマーをリセットしているわけです。
12 行目からは、エラーハンドリングとなります。
eval
から抜け出した場合、何かしらのエラーがあれば、$@ にエラー内容が格納されているはずです。
タイムアウトした場合には、2 行目の die
で指定したエラー内容がそのまま格納されているはずです。
従って、$@
の内容によって、場合分けをし、それぞれの処理を記述します。