私は質問を数回読みました、そして私はあなたがやろうとしていることをある程度理解していると思います。制御スクリプトがあります。このスクリプトは、子を生成していくつかのことを実行し、これらの子は孫を生成して実際に作業を実行します。問題は、孫が遅すぎる可能性があり(STDINなどを待っている)、あなたが孫を殺したいということです。さらに、遅い孫が1人いる場合は、子供全体を死なせます(可能であれば、他の孫を殺します)。
そこで、この2つの方法を実装してみました。1つ目は、親が新しいUNIXセッションで子を生成し、タイマーを数秒に設定し、タイマーが切れたときに子セッション全体を強制終了することでした。これにより、親は子供と孫の両方に責任を持つようになりました。また、正しく機能しませんでした。
次の戦略は、親に子をスポーンさせてから、子に孫の管理を任せることでした。孫ごとにタイマーを設定し、有効期限までにプロセスが終了していなかった場合はタイマーを強制終了します。これはうまく機能するので、ここにコードがあります。
EVを使用して子とタイマーを管理し、AnyEventをAPIに使用します。(EventやPOEなどの別のAnyEventイベントループを試すことができます。ただし、EVは、ループに監視するように指示する前に、子が終了する状態を正しく処理することを知っています。これにより、他のループが脆弱である煩わしい競合状態が排除されます。)
#!/usr/bin/env perl
use strict;
use warnings;
use feature ':5.10';
use AnyEvent;
use EV; # you need EV for the best child-handling abilities
チャイルドウォッチャーを追跡する必要があります。
# active child watchers
my %children;
次に、子を起動する関数を作成する必要があります。親がスポーンするものは子と呼ばれ、子がスポーンするものはジョブと呼ばれます。
sub start_child($$@) {
my ($on_success, $on_error, @jobs) = @_;
引数は、子が正常に完了したときに呼び出されるコールバック(つまり、そのジョブも成功したことを意味します)、子が正常に完了しなかったときに呼び出されるコールバック、および実行するcoderefジョブのリストです。
この関数では、フォークする必要があります。親では、子を監視するために子ウォッチャーを設定します。
if(my $pid = fork){ # parent
# monitor the child process, inform our callback of error or success
say "$$: Starting child process $pid";
$children{$pid} = AnyEvent->child( pid => $pid, cb => sub {
my ($pid, $status) = @_;
delete $children{$pid};
say "$$: Child $pid exited with status $status";
if($status == 0){
$on_success->($pid);
}
else {
$on_error->($pid);
}
});
}
子供の中で、私たちは実際に仕事をします。ただし、これには少しセットアップが必要です。
まず、親の子ウォッチャーを忘れます。これは、子がその兄弟の退出について通知されることは意味がないためです。(フォークは楽しいです。それがまったく意味をなさない場合でも、親の状態をすべて継承するからです。)
else { # child
# kill the inherited child watchers
%children = ();
my %timers;
また、すべての仕事がいつ完了したか、そしてそれらがすべて成功したかどうかを知る必要があります。カウント条件変数を使用して、すべてがいつ終了したかを判別します。起動時にインクリメントし、終了時にデクリメントします。カウントが0の場合、すべてが完了したことがわかります。
また、エラー状態を示すためにブール値を保持します。プロセスがゼロ以外のステータスで終了する場合、エラーは1になります。それ以外の場合、プロセスは0のままです。これよりも多くの状態を保持することをお勧めします:)
# then start the kids
my $done = AnyEvent->condvar;
my $error = 0;
$done->begin;
(また、カウントを1から開始して、ジョブが0の場合でも、プロセスが終了するようにします。)
次に、ジョブごとにフォークして、ジョブを実行する必要があります。親では、いくつかのことを行います。condvarをインクリメントします。遅すぎる場合に子供を殺すためにタイマーを設定しました。また、子ウォッチャーを設定して、ジョブの終了ステータスを通知できるようにします。
for my $job (@jobs) {
if(my $pid = fork){
say "[c] $$: starting job $job in $pid";
$done->begin;
# this is the timer that will kill the slow children
$timers{$pid} = AnyEvent->timer( after => 3, interval => 0, cb => sub {
delete $timers{$pid};
say "[c] $$: Killing $pid: too slow";
kill 9, $pid;
});
# this monitors the children and cancels the timer if
# it exits soon enough
$children{$pid} = AnyEvent->child( pid => $pid, cb => sub {
my ($pid, $status) = @_;
delete $timers{$pid};
delete $children{$pid};
say "[c] [j] $$: job $pid exited with status $status";
$error ||= ($status != 0);
$done->end;
});
}
タイマーは状態を保持するため、タイマーの使用はアラームよりも少し簡単です。各タイマーはどのプロセスを強制終了するかを知っており、プロセスが正常に終了したときにタイマーを簡単にキャンセルできます。ハッシュからタイマーを削除するだけです。
それが(子の)親です。(子の、または仕事の)子は本当に単純です:
else {
# run kid
$job->();
exit 0; # just in case
}
必要に応じて、ここでstdinを閉じることもできます。
ここで、すべてのプロセスが生成された後、condvarを待機して、すべてのプロセスが終了するのを待ちます。イベントループは子とタイマーを監視し、私たちのために正しいことをします:
} # this is the end of the for @jobs loop
$done->end;
# block until all children have exited
$done->recv;
次に、すべての子が終了したら、次のようなクリーンアップ作業を実行できます。
if($error){
say "[c] $$: One of your children died.";
exit 1;
}
else {
say "[c] $$: All jobs completed successfully.";
exit 0;
}
} # end of "else { # child"
} # end of start_child
OK、それが子供と孫/仕事です。ここで、親を作成する必要があります。これははるかに簡単です。
子供のように、カウント条件を使用して子供を待ちます。
# main program
my $all_done = AnyEvent->condvar;
やるべき仕事が必要です。これは常に成功するものであり、returnキーを押すと成功しますが、タイマーで強制終了するだけでは失敗します。
my $good_grandchild = sub {
exit 0;
};
my $bad_grandchild = sub {
my $line = <STDIN>;
exit 0;
};
したがって、子ジョブを開始する必要があります。の先頭に戻る方法を覚えている場合はstart_child
、エラーコールバックと成功コールバックの2つのコールバックが必要です。それらを設定します。エラーコールバックは「notok」を出力してcondvarをデクリメントし、成功コールバックは「ok」を出力して同じことを行います。とてもシンプルです。
my $ok = sub { $all_done->end; say "$$: $_[0] ok" };
my $nok = sub { $all_done->end; say "$$: $_[0] not ok" };
次に、さらに多くの孫の仕事をしているたくさんの子供たちを始めることができます:
say "starting...";
$all_done->begin for 1..4;
start_child $ok, $nok, ($good_grandchild, $good_grandchild, $good_grandchild);
start_child $ok, $nok, ($good_grandchild, $good_grandchild, $bad_grandchild);
start_child $ok, $nok, ($bad_grandchild, $bad_grandchild, $bad_grandchild);
start_child $ok, $nok, ($good_grandchild, $good_grandchild, $good_grandchild, $good_grandchild);
それらのうちの2つはタイムアウトになり、2つは成功します。ただし、実行中にEnterキーを押すと、すべて成功する可能性があります。
とにかく、それらが開始されたら、それらが終了するのを待つ必要があります。
$all_done->recv;
say "...done";
exit 0;
そしてそれがプログラムです。
Parallel :: ForkManagerが行っていないことの1つはn
、一度に子だけが実行されるようにフォークを「レート制限」することです。ただし、これは手動で実装するのは非常に簡単です。
use Coro;
use AnyEvent::Subprocess; # better abstraction than manually
# forking and making watchers
use Coro::Semaphore;
my $job = AnyEvent::Subprocess->new(
on_completion => sub {}, # replace later
code => sub { the child process };
)
my $rate_limit = Coro::Semaphore->new(3); # 3 procs at a time
my @coros = map { async {
my $guard = $rate_limit->guard;
$job->clone( on_completion => Coro::rouse_cb )->run($_);
Coro::rouse_wait;
}} ({ args => 'for first job' }, { args => 'for second job' }, ... );
# this waits for all jobs to complete
my @results = map { $_->join } @coros;
ここでの利点は、子が実行されている間に他のことを実行できることですasync
。ブロッキング結合を実行する前に、より多くのスレッドを生成するだけです。また、AnyEvent :: Subprocessを使用して、子をより詳細に制御できます。子をPtyで実行し、stdinにフィードし(Expectのように)、そのstdinとstdoutとstderrをキャプチャするか、無視することができます。それらのもの、または何でも。物事を「シンプル」にしようとしているモジュール作成者ではなく、あなたが決めることができます。
とにかく、これが役立つことを願っています。