@kyanny's blog

My thoughts, my life. Views/opinions are my own.

実行中のプロセスが終わるまで待つ SIGINT のトラップの仕方

追記

牧さんがhttp://mt.endeworks.jp/d-6/2009/11/perl-4.htmlで詳細に書いてくれてるのでそっちを見るといいです。 lestrrat++

シグナルハンドラの中で SIGINT とかを無効にしたい - 刺身☆ブーメランのはてなダイアリー で「うまくいかない」と書いてたやつの解決編。のつもり。

やりたかったことは、「時間がかかる処理を実行中の Perl スクリプトが SIGINT を受け取ったとき、すでに実行中の処理が終わるまで待ってから死んで欲しい」というもの。

コードそのものは実は大してかわってない。どうも、 SIGINT などが送られたときに「Perl のステートメントひとつの単位で処理に割り込みが入る」ような挙動になってるらしく、終わるまで待って欲しい処理(以下の例だと foo() ないし bar() 関数の実行)の中が例えば sleep 5; のようになっていると、「五秒待つ」というのが Perl にとってアトミックな処理なので、丸ごと割り込まれてしまう(・・・ので、先日のエントリの例だと三秒待ってから終了して欲しいのに、実際には三秒経たずに終了してしまう)、という風になっているようだ(説明が意味不明なのは僕も正確に理解しているわけではなく、 id:nabokov に教えてもらった通りに書いたら動いた!とりあえずメモっとこ!・・・だからです)

#!/usr/bin/env perl
use strict;
use DateTime;

our $cnt = 0;
our $int = 0;

$SIG{INT} = sub {
    warn sprintf('sigint received at %s.', DateTime->now(time_zone => 'local'));
    $int++;
};

while (1) {
    warn 'CNT: '. $cnt;
    foo();
    warn 'CNT: '. $cnt;
    if ($int) {
        warn sprintf('interrupted at %s.', DateTime->now(time_zone => 'local'));
        exit 1;
    }
}

sub foo {
    warn 'start foo()';
    sleep 1;sleep 1;sleep 1;sleep 1;sleep 1; # 5 sec
    for (1..50) {
        $cnt++;
    }
    bar();
    sleep 1;sleep 1;sleep 1;sleep 1;sleep 1; # 5 sec
    warn 'end foo()';
}
sub bar {
    warn 'start bar()';
    sleep 1;sleep 1;sleep 1;sleep 1;sleep 1; # 5 sec
    for (1..50) {
        $cnt++;
    }
    sleep 1;sleep 1;sleep 1;sleep 1;sleep 1; # 5 sec
    warn 'end bar()';
}

実行結果はこんな風になる。実行中に何度か Ctrl-C を受け取っているけどそのまま処理が続いてることがわかる。

$ ./sigint.pl
CNT: 0 at ./sigint.pl line 14.
start foo() at ./sigint.pl line 24.
^Csigint received at 2009-11-24T19:19:25. at ./sigint.pl line 9.
start bar() at ./sigint.pl line 34.
^Csigint received at 2009-11-24T19:19:32. at ./sigint.pl line 9.
end bar() at ./sigint.pl line 40.
^Csigint received at 2009-11-24T19:19:40. at ./sigint.pl line 9.
end foo() at ./sigint.pl line 31.
CNT: 100 at ./sigint.pl line 16.
interrupted at 2009-11-24T19:19:44. at ./sigint.pl line 18.