Ответ 1
Я несколько раз прочел вопрос, и я думаю, что я получаю то, что вы пытаются сделать. У вас есть элемент управления script. Этот script нерестится дети, чтобы сделать что-то, и эти дети порождают внуков на самом деле делать работу. Проблема в том, что внуки могут быть слишком медленно (ожидание STDIN или что-то еще), и вы хотите их убить. Кроме того, если есть один медленный внук, вы хотите, чтобы весь ребенок умирает (убивая других внуков, если это возможно).
Итак, я попытался реализовать эти два пути. Первым было сделать parent порождает ребенка в новом сеансе 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([email protected]) {
my ($on_success, $on_error, @jobs) = @_;
Аргументы - это обратный вызов, вызываемый при завершении дочернего процесса успешно (то есть его рабочие места также были успешными), обратный вызов, когда ребенок не завершил успешно, а затем список coderef выполняемые задания.
В этой функции нам нужно использовать fork. В родителе мы устанавливаем дочерний элемент наблюдатель для наблюдения за ребенком:
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);
}
});
}
В ребенке мы фактически запускаем задания. Это связано с немного setup. Тем не менее.
Во-первых, мы забываем о родительских наблюдателях детей, потому что это не делает смысл для ребенка быть информированным о своих братьях и сестрах. (Вилка весело, потому что вы наследуете все родительское состояние, даже если это не имеет никакого смысла.)
else { # child
# kill the inherited child watchers
%children = ();
my %timers;
Нам также нужно знать, когда все задания выполняются, и независимо от того, все они были успешными. Мы используем условную переменную counting определить, когда все вышло. Мы приступаем к запуску, и декремент при выходе, а когда число равно 0, мы знаем, что все сделано.
Я также сохраняю логическое значение, чтобы указать состояние ошибки. Если процесс выходы с ненулевым статусом, ошибка переходит к 1. В противном случае он остается 0. Возможно, вы захотите сохранить больше состояний, чем это:)
# then start the kids
my $done = AnyEvent->condvar;
my $error = 0;
$done->begin;
(Мы также начинаем подсчет в 1, так что если есть 0 заданий, наш процесс все еще выходит.)
Теперь нам нужно использовать fork для каждого задания и запускать задание. В родителе мы сделайте несколько вещей. Мы увеличиваем конвар. Мы устанавливаем таймер, чтобы убить ребенок, если он слишком медленный. И мы настраиваем наблюдателя за детьми, поэтому мы можем получать информацию о статусе выхода на работу.
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
Хорошо, так что ребенок и внук/работа. Теперь нам просто нужно написать родителя, что намного проще.
Как и ребенок, мы собираемся использовать счетчик, чтобы дождаться нашего детей.
# 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
, он принимает два обратных вызова, ошибку
обратный вызов и успешный обратный вызов. Мы подберем их; Ошибка
обратный вызов будет печатать "не в порядке" и уменьшать конвер, а
успешный обратный вызов будет печатать "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);
Два из них будут тайм-аут, а два будут успешными. Если вы нажмете enter хотя они все же работают, тогда все они могут преуспеть.
В любом случае, как только они начнутся, нам просто нужно дождаться их Отделка:
$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 и фиде
это stdin (например, с Expect), и вы можете записать его stdin и stdout
и stderr, или вы можете игнорировать эти вещи или что-то еще. Вы добираетесь до
решите, а не некоторый автор модуля, который пытается сделать вещи "простыми".
В любом случае, надеюсь, что это поможет.