He leído la pregunta unas cuantas veces, y creo que de alguna manera consigo lo que está tratando de hacer. Usted tiene un script de control. Este script genera hijos para hacer algunas cosas, y estos hijos engendran a los nietos para realmente hacer el trabajo. El problema es que los nietos pueden ser demasiado lento (esperando STDIN, o lo que sea), y desea matarlos. Además, si hay un nieto lento, quiere que muera todo el hijo (matando a los otros nietos, si es posible).
Por lo tanto, traté de implementar esto de dos maneras. La primera era hacer que el padre engendrara un hijo en una nueva sesión de UNIX, establecer un temporizador durante unos pocos segundos y matar toda la sesión secundaria cuando se disparaba el temporizador. Esto hizo que el padre se responsabilizara tanto del hijo como de los nietos . Tampoco funcionó bien.
La siguiente estrategia fue hacer que el padre engendre el hijo, y luego hacer que el niño sea responsable de administrar a los nietos. Sería establecer un temporizador para cada nieto, y matarlo si el proceso no tenía salido por el tiempo de caducidad. Esto funciona muy bien, así que aquí está el código.
Usaremos EV para administrar los elementos secundarios y temporizadores, y AnyEvent para la API . (Puede probar otro bucle de evento AnyEvent, como Evento o POE. Pero sé que EV maneja correctamente la condición en la que un niño sale de antes de decirle al bucle que lo monitorice, lo que elimina las molestas condiciones de raza que otros bucles son vulnerables a .)
#!/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
necesitamos hacer un seguimiento de los observadores de los niños:
# active child watchers
my %children;
entonces tenemos que escribir una función para iniciar a los niños. Las cosas que generan los padres se llaman hijos, y las cosas que los hijos engendran se llaman trabajos.
sub start_child([email protected]) {
my ($on_success, $on_error, @jobs) = @_;
Los argumentos son una devolución de llamada a ser llamado cuando el niño completa éxito (es decir, su empleo también fueron un éxito), una devolución de llamada cuando el niño no se ha completado satisfactoriamente, y luego una lista de coderef empleos correr.
En esta función, tenemos que bifurcar. En el padre, que configurar un observador niño de monitorizar los niños:
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);
}
});
}
en el niño, que en realidad ejecutar los trabajos. Esto implica un poco de configuración , sin embargo.
En primer lugar, nos olvidamos de los vigilantes de los hijos de los padres, ya que no hace que tenga sentido para que el niño se entere de que sus hermanos están saliendo. (Tenedor es divertido, porque se hereda todos estado de los padres, aun cuando ese no tiene ningún sentido en absoluto.)
else { # child
# kill the inherited child watchers
%children =();
my %timers;
También tenemos que saber cuando se hacen todos los puestos de trabajo, y si o no todos fueron un éxito. Utilizamos una variable condicional de recuento en para determinar cuándo ha salido todo. Incrementamos al inicio, y decremento a la salida, y cuando el conteo es 0, sabemos que todo está hecho.
También tengo un booleano para indicar el estado de error. Si un proceso sale con un estado distinto de cero, el error va a 1. De lo contrario, se queda 0. Es posible que desee mantener más el estado de esta :)
# then start the kids
my $done = AnyEvent->condvar;
my $error = 0;
$done->begin;
(También iniciar la cuenta en 1 de modo que si hay 0 trabajos, nuestro proceso todavía sale.)
Ahora tenemos que bifurcar para cada trabajo y ejecutar el trabajo. En el padre, nosotros hacemos algunas cosas. Incrementamos el condvar. Configuramos un temporizador para matar al niño si es demasiado lento. Y configuramos un vigilante de niños, para que podamos informarnos del estado de salida del trabajo al .
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;
});
}
Uso del temporizador es un poco más fácil que la alarma, ya que lleva estado con ella. Cada temporizador sabe qué proceso matar, y es fácil cancelar el temporizador cuando el proceso finaliza correctamente; simplemente lo eliminamos del hash.
Ese es el padre (del niño). El niño (del niño, o el trabajo ) es muy simple:
else {
# run kid
$job->();
exit 0; # just in case
}
Usted podría también cerca de la entrada estándar aquí, si quería.
Ahora, después de que todos los procesos se hayan engendrado, los esperamos a todos salen esperando al condvar.El ciclo de eventos se monior los niños y contadores de tiempo, y hacer lo correcto para nosotros:
} # this is the end of the for @jobs loop
$done->end;
# block until all children have exited
$done->recv;
Entonces, cuando todos los niños han salido, podemos hacer cualquier limpieza trabajo queremos, como:
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, entonces ese es el trabajo de niño y nieto /. Ahora solo tenemos que escribir el padre, que es mucho más fácil.
Al igual que el niño, vamos a utilizar un condvar contar para esperar nuestros niños.
# main program
my $all_done = AnyEvent->condvar;
Necesitamos algunos trabajos para hacer. Aquí hay una que siempre tiene éxito, y que será exitosa si se pulsa el retorno, pero fallará si acaba de dejarlo ser matado por el temporizador:
my $good_grandchild = sub {
exit 0;
};
my $bad_grandchild = sub {
my $line = <STDIN>;
exit 0;
};
Así que sólo tenemos que empezar el niño trabajos. Si recuerda el modo en la parte superior de start_child
, se requieren dos devoluciones de llamada, un error de devolución de llamada, y una devolución de llamada exitosa. Los configuraremos; el error de devolución de llamada imprimirá "no está bien" y disminuirá la condición, y la devolución de llamada satisfactoria imprimirá "ok" y hará lo mismo. Muy simple.
my $ok = sub { $all_done->end; say "$$: $_[0] ok" };
my $nok = sub { $all_done->end; say "$$: $_[0] not ok" };
Entonces podemos empezar un grupo de niños con aún más nietos empleos:
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);
Dos de ellos será el tiempo de espera, y dos tendremos éxito. Sin embargo, si presiona enter mientras se están ejecutando, todos podrían tener éxito.
De todos modos, una vez que los han comenzado, sólo tenemos que esperar a que se acabado:
$all_done->recv;
say "...done";
exit 0;
Y ese es el programa.
Una cosa que no estamos haciendo que paralelo :: ForkManager hace es "Limitación de velocidad" nuestros tenedores de manera que sólo n
los niños se están ejecutando en un momento . Esto es bastante fácil de implementar de forma manual, sin embargo:
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;
La ventaja aquí es que usted puede hacer otras cosas mientras sus hijos se están ejecutando - acaba de generar más hilos con async
antes de hacer el bloqueo unirse. También tiene mucho más control sobre los hijos con AnyEvent :: Subproceso - puede ejecutar el hijo en una Pty y alimentar stdin (como con Esperar), y puede capturar su stdin y stdout y stderr, o puedes ignorar esas cosas, o lo que sea. Llegas a decide, no un autor de módulo que está tratando de hacer las cosas "simples".
De todos modos, espero que esto ayude.
¿Hay algún motivo por el que no cierre STDIN en los niños? – jrockway
Eso también podría ser una buena idea. Tendré que pensar en eso, aunque estaba considerando manejar algunos de los casos malos con algo como Esperar para que hagan lo que tienen que hacer. –
Consulte la respuesta larga a continuación. Pero, básicamente, creo que necesitas más control sobre la situación que Parallel :: ForkManager te permite, lo que significa que tienes que hacer tu propia. – jrockway