タイムアウト処理

処理に時間がかかりそうな処理を書く際、タイムアウト時間を設定して、それを超えても処理が完了しなければ、 エラーを表示するような処理が必要になることがあります。 特に、ソケットを使って外部サーバーと通信する場合などがあてはまります。 もし相手のサーバーからの応答が無かったらどうなるでしょうか。 応答があるまで待ち続けることになります。 または、ブラウザー側が見切りをつけて待機することをやめてしまうでしょう。 もしご利用のレンタルサーバーに 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 を使うと、例外が発生したとみなされ、 その内容は $@ に格納され、スクリプトは終了しません。 dieeval を抜け出した後の処理は、12 行目からとなります。

10 行目にも alarm 0; がありますが、これは必要です。 もし 7 行目の alarm 0; に到達する前に時間切れになった場合、 どこかでタイマーをキャンンセルしなければいけないからです。 従って、eval の処理を抜けた直後にタイマーをリセットしているわけです。

12 行目からは、エラーハンドリングとなります。 eval から抜け出した場合、何かしらのエラーがあれば、$@ にエラー内容が格納されているはずです。 タイムアウトした場合には、2 行目の die で指定したエラー内容がそのまま格納されているはずです。 従って、$@ の内容によって、場合分けをし、それぞれの処理を記述します。