我已经读了几次这个问题,我想我有点明白你的意思
正在努力做。你有一个控制脚本。这个脚本产生
孩子做一些事情,这些孩子产生了孙子
实际做这项工作。问题是孙子们可以
太慢(等待 STDIN 或其他),你想杀死它们。
此外,如果有一个慢孙子,你想要整个
孩子死(如果可能,杀死其他孙子)。
所以,我尝试了这两种方式。首先是制作
父母在一个新的 UNIX 会话中产生一个孩子,设置一个计时器
秒,并在计时器关闭时终止整个子会话。
这使得父母对孩子和孩子都负有责任
孙子。它也不能正常工作。
下一个策略是让父母产生孩子,然后
让孩子负责管理孙辈。它会
为每个孙子设置一个计时器,如果进程没有,则将其杀死
到期时间退出。这很好用,所以这里是代码。
我们将使用 EV 来管理孩子和计时器,并使用 AnyEvent 来管理
API。 (您可以尝试另一个 AnyEvent 事件循环,例如 Event 或 POE。
但我知道 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;
然后我们需要编写一个函数来启动孩子。这些事
父母产生的称为孩子,而孩子的东西
spawn 称为作业。
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
}
如果您愿意,也可以在此处关闭标准输入。
现在,在所有进程都生成之后,我们等待它们
通过等待 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
好的,这就是孩子和孙子/工作。现在我们只需要写
父母,这要容易得多。
像孩子一样,我们将使用计数 condvar 来等待我们的
孩子们。
# main program
my $all_done = AnyEvent->condvar;
我们需要做一些工作。这是一个总是成功的,并且
如果您按回车键将成功,但如果您按回车键将失败
让它被计时器杀死:
my $good_grandchild = sub {
exit 0;
};
my $bad_grandchild = sub {
my $line = <STDIN>;
exit 0;
};
那么我们只需要启动子作业。如果你记得方式
回到start_child的顶部,需要两次回调,报错
回调和成功回调。我们将设置它们;错误
回调将打印“not ok”并减少 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);
其中两个将超时,两个将成功。如果按回车
不过,当它们在运行时,它们可能都会成功。
无论如何,一旦开始,我们只需要等待他们
完成:
$all_done->recv;
say "...done";
exit 0;
这就是程序。
Parallel::ForkManager 没有做的一件事是
“速率限制”我们的分叉,以便只有 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 中运行子进程并提要
它的标准输入(与 Expect 一样),您可以捕获它的标准输入和标准输出
和stderr,或者你可以忽略这些东西,或者其他什么。你得到
决定,而不是一些试图让事情变得“简单”的模块作者。
无论如何,希望这会有所帮助。