Parallel::ForkManagerの罠
CPANに並列実行を賢く制御してくれるParallel::ForkManagerというモジュールがある. そこそこ使われている有名モジュールっぽいのだけど,軽く嵌まったのでメモ.
Parallel::ForkManagerの使い方
Parallel::ForkManager
は,最大同時実行数を制限した並列実行をとっても簡単に実現してくれる.
use Parallel::ForkManager; $pm->Parallel::ForkManager( 4 ); # 子プロセスの最大数を 4 に設定 foreach my $piece ( @list ) { $pid = $pm->start and next; # start は fork と同じく,親なら子のPID, 子なら0を返す some_processing( $piece ); # 子プロセスが行なう処理を記述 $pm->finish; # 子プロセスの終了 } $pm->wait_all_children; # 全ての子プロセスの終了を待つ
他にも,子プロセスの開始時・終了時に親が行なう処理を指定できたり(run_on_start
/ run_on_finish
),プロセス数制限のために子プロセスの起動待ちをする際に実行する処理を指定したり (run_on_wait
) できる.
嵌まっていた罠
お気楽に書いたコードでも嵌まるときは嵌まる. 以下のような問題に立ちあたってしまった.
Parallel::ForkManager
とNet::SCP::Expect
を一緒に使うと,$pm->start
あるいは$pm->wait_all_children
のタイミングでプログラムが無限ループ
因みにParallel::ForkManager
のバージョンは 0.7.5 だ.
なにが原因なのか
Parallel::ForkManager
は内部的に子プロセスのリストを自分で管理している.
簡単に述べると,次のような処理を行っている.
- 子プロセスの開始 (
$pm->start
) 時に子プロセス PID を覚える - 子プロセスの終了を
waitpid(-1, 0)
で回収し,終了したプロセスの PID を忘れる
自分で子プロセスのリストを管理しているということは,
管理できない方法で子プロセスがスタートしたり,預り知らぬ間に
終了してwait
で回収されたりすると困るということだ.
しかし,実はNet::SCP::Expect
もfork
とwait
を使うモジュールであり,子プロセスが無駄にゾンビ化しないように気を配ってくれている.
具体的には,以下のような SIGCHLD
ハンドラをBEGIN
ブロックで設定している.
$SIG{CHLD} = \&reapChild; sub reapChild { do {} while waitpid(-1, WNOHANG) > 0; }
SIGCHLD
は子プロセスの終了時に親プロセスに届くシグナルであり,ハンドラreapChild
で終了したプロセスを看取るループを回している.
これは子プロセスを回収するための非常に一般的なやり方であり,Net::SCP::Expect
独自というわけではないが,なんにせよコレが子プロセスが死ぬと真っ先に回収してくれるわけだ.
つまり,この「子プロセスを素早く回収する」シグナルハンドラが設定されることにより,
Parallel::ForkManager
よりも先にシグナルハンドラが子プロセスを看取ってしまう.
よって Parallel::ForkManager
の子プロセスリストは正常に管理されなくなり,
既に死んでいる子をいつまでも待ち続けた,というのが今回の問題の原因.
とりあえず今回の問題は以下のコードで乗り切れる.
$sig_chld_handler_backup = $SIG{CHLD}; # SIGCHLD ハンドラを記憶 $SIG{CHLD} = sub {}; # ForkManager のために SIGCHLD ハンドラを削除 foreach my $piece ( @list ) { $pid = $pm->start and next; $SIG{CHLD} = $sig_chld_handler_backup; # 子プロセスにのみ SIGCHLD ハンドラを復帰 some_processing( $piece ); # 子プロセスが行なう処理を記述 $pm->finish; } $pm->wait_all_children;
補足だが,Parallel::ForkManager
のマニュアルには「コードの他の部分でfork()/wait()
するような環境では使用しないほうが良い」と書かれている.
しかし使っているモジュールが内部で何しているかなんてなかなかわからない.