¿Cómo esperar en bash para que terminen varios subprocesos y devolver el código de salida! = 0 cuando cualquier subproceso termina con el código! = 0?

563

¿Cómo esperar en un script bash varios subprocesos generados desde ese script para finalizar y devolver el código de salida! = 0 cuando alguno de los subprocesos termina con el código! = 0?

Script simple:

#!/bin/bash
for i in `seq 0 9`; do
  doCalculations $i &
done
wait

El script anterior esperará a los 10 subprocesos generados, pero siempre dará el estado de salida 0 (ver help wait). ¿Cómo puedo modificar este script para que descubra los estados de salida de los subprocesos generados y devuelva el código de salida 1 cuando alguno de los subprocesos termina con el código! = 0?

¿Hay alguna solución mejor para eso que recolectar los PID de los subprocesos, esperarlos en orden y sumar los estados de salida?

tkokoszka
fuente
1
Esto podría mejorarse significativamente al tocar wait -n, disponible en bash moderno para regresar solo cuando se complete el primer / siguiente comando.
Charles Duffy
si está buscando probar usando Bash, intente esto: github.com/sstephenson/bats
Alexander Mills
2
El desarrollo activo de BATS se ha trasladado a github.com/bats-core/bats-core
Potherca
3
@CharlesDuffy wait -ntiene un pequeño problema: si no quedan trabajos secundarios restantes (también conocido como condición de carrera), devuelve un estado de salida distinto de cero (fallo) que puede ser indistinguible de un proceso secundario fallido.
drevicko
55
@CharlesDuffy: tienes una visión maravillosa y haces un gran servicio a SO al compartirlo. Parece que alrededor del 80% de las publicaciones de SO que leí tienen que compartir maravillosos pequeños diamantes de conocimiento en los comentarios que deben provenir de un vasto océano de experiencia. ¡Muchas gracias!
Brett Holman el

Respuestas:

521

waittambién (opcionalmente) toma el PID del proceso para esperar, y con $! obtienes el PID del último comando lanzado en segundo plano. Modifique el bucle para almacenar el PID de cada subproceso generado en una matriz, y luego vuelva a bucle esperando cada PID.

# run processes and store pids in array
for i in $n_procs; do
    ./procs[${i}] &
    pids[${i}]=$!
done

# wait for all pids
for pid in ${pids[*]}; do
    wait $pid
done
Luca Tettamanti
fuente
99
Bueno, dado que va a esperar todos los procesos, no importa si, por ejemplo, está esperando el primero mientras el segundo ya ha finalizado (el segundo se seleccionará en la próxima iteración de todos modos). Es el mismo enfoque que usarías en C con wait (2).
Luca Tettamanti
77
Ah, ya veo - interpretación diferente :) Leí la pregunta que significa "devolver el código de salida 1 inmediatamente cuando cualquiera de los subprocesos sale".
Alnitak
56
El PID se puede reutilizar de hecho, pero no puede esperar un proceso que no sea secundario del proceso actual (la espera falla en ese caso).
tkokoszka
12
También puede usar% n para referirse al trabajo en segundo plano n: th y %% para referirse al más reciente.
conny
30
@Nils_M: Tienes razón, lo siento. Entonces sería algo como: for i in $n_procs; do ./procs[${i}] & ; pids[${i}]=$!; done; for pid in ${pids[*]}; do wait $pid; done;¿verdad?
synack
285

http://jeremy.zawodny.com/blog/archives/010717.html :

#!/bin/bash

FAIL=0

echo "starting"

./sleeper 2 0 &
./sleeper 2 1 &
./sleeper 3 0 &
./sleeper 2 0 &

for job in `jobs -p`
do
echo $job
    wait $job || let "FAIL+=1"
done

echo $FAIL

if [ "$FAIL" == "0" ];
then
echo "YAY!"
else
echo "FAIL! ($FAIL)"
fi
HoverHell
fuente
104
jobs -pestá dando PID de subprocesos que están en estado de ejecución. Omitirá un proceso si el proceso finaliza antes de que jobs -pse llame. Entonces, si alguno de los subprocesos termina antes jobs -p, el estado de salida de ese proceso se perderá.
tkokoszka
15
Wow, esta respuesta es mucho mejor que la mejor calificada. : /
e40
44
@ e40 y la respuesta a continuación es probablemente aún mejor. Y aún mejor probablemente sería ejecutar cada comando con '(cmd; echo "$?" >> "$ tmpfile"), usar esta espera y luego leer el archivo para los errores. También anotar salida. ... o simplemente usa este script cuando no te importa tanto.
HoverHell
Me gustaría agregar que esta respuesta es mejor que la aceptada
shurikk
2
@tkokoszka para ser exactos jobs -pno está dando PID de subprocesos, sino GPID . La lógica de espera parece funcionar de todos modos, siempre espera en el grupo si dicho grupo existe y pide si no, pero es bueno tener en cuenta ... especialmente si uno se basa en esto e incorpora algo como enviar mensajes al subproceso en el que caso la sintaxis es diferente dependiendo de si tiene PID o GPID .. es decir, kill -- -$GPIDvskill $PID
Timo
59

Aquí hay un ejemplo simple de uso wait.

Ejecute algunos procesos:

$ sleep 10 &
$ sleep 10 &
$ sleep 20 &
$ sleep 20 &

Luego espere con el waitcomando:

$ wait < <(jobs -p)

O simplemente wait(sin argumentos) para todos.

Esto esperará a que se completen todos los trabajos en segundo plano.

Si -nse proporciona la opción, espera a que termine el siguiente trabajo y devuelve su estado de salida.

Ver: help waity help jobspara la sintaxis.

Sin embargo, la desventaja es que esto solo devolverá el estado de la última ID, por lo que debe verificar el estado de cada subproceso y almacenarlo en la variable.

O haga su función de cálculo para crear algún archivo en caso de falla (vacío o con registro de fallas), luego verifique ese archivo si existe, por ejemplo

$ sleep 20 && true || tee fail &
$ sleep 20 && false || tee fail &
$ wait < <(jobs -p)
$ test -f fail && echo Calculation failed.
kenorb
fuente
2
Para aquellos nuevos en bash, los dos cálculos en el ejemplo aquí son sleep 20 && truey sleep 20 && false, es decir, reemplazar aquellos con sus funciones. Para comprender &&y ||ejecutar man bashy escribir '/' (buscar), luego '^ * Listas' (una expresión regular) y luego ingresar: man se desplazará hacia abajo hasta la descripción de &&y||
drevicko
1
Probablemente deberías comprobar que el archivo 'fail' no existe al inicio (o eliminarlo). Dependiendo de la aplicación, también podría ser una buena idea agregar '2> & 1' antes de que el ||STDERR falle también.
drevicko
Me gusta este, ¿algún inconveniente? en realidad, solo cuando quiero enumerar todos los subprocesos y tomar algunas acciones, por ejemplo. enviar señal, que intentaré contabilizar pids o iterar trabajos. Espere a que termine, solowait
xgwang
Esto perderá el estado de salida del trabajo que falló antes de que se llame a trabajos -p
Erik Aronesty
50

Si tiene GNU Parallel instalado, puede hacer:

# If doCalculations is a function
export -f doCalculations
seq 0 9 | parallel doCalculations {}

GNU Parallel le dará el código de salida:

  • 0: todos los trabajos se ejecutaron sin error.

  • 1-253 - Algunos de los trabajos fallaron. El estado de salida da el número de trabajos fallidos

  • 254: más de 253 trabajos fallaron.

  • 255 - Otro error.

Mire los videos de introducción para obtener más información: http://pi.dk/1

Ole Tange
fuente
1
¡Gracias! Pero olvidó mencionar el problema de "confusión" en el que posteriormente me encontré
nobar
1
Esto parece una gran herramienta, pero no creo que lo anterior funcione como está en un script Bash donde doCalculationshay una función definida en ese mismo script (aunque el OP no estaba claro sobre este requisito). Cuando lo intento, paralleldice /bin/bash: doCalculations: command not found(dice esto 10 veces para el seq 0 9ejemplo anterior). Vea aquí para una solución alternativa.
nobar
3
También de interés: xargstiene cierta capacidad para iniciar trabajos en paralelo a través de la -Popción. A partir de aquí : export -f doCalculations ; seq 0 9 |xargs -P 0 -n 1 -I{} bash -c "doCalculations {}". Las limitaciones de xargsse enumeran en la página del manual para parallel.
nobar
Y si se doCalculationsbasa en otras variables de entorno internas de script (personalizadas PATH, etc.), probablemente necesiten ser editadas explícitamente exportantes de iniciarse parallel.
nobar
44
@nobar La confusión se debe a que algunos empaquetadores arruinan las cosas para sus usuarios. Si instala utilizando wget -O - pi.dk/3 | shno obtendrá confusiones. Si su empaquetador lo ha estropeado, le animo a que plantee el problema con su empaquetador. Las variables y funciones deben exportarse (export -f) para que GNU Parallel las vea (ver man parallel: gnu.org/software/parallel/… )
Ole Tange
46

¿Qué tal simplemente:

#!/bin/bash

pids=""

for i in `seq 0 9`; do
   doCalculations $i &
   pids="$pids $!"
done

wait $pids

...code continued here ...

Actualizar:

Como señalaron varios comentaristas, lo anterior espera a que se completen todos los procesos antes de continuar, pero no sale y falla si uno de ellos falla, se puede hacer con la siguiente modificación sugerida por @Bryan, @SamBrightman y otros :

#!/bin/bash

pids=""
RESULT=0


for i in `seq 0 9`; do
   doCalculations $i &
   pids="$pids $!"
done

for pid in $pids; do
    wait $pid || let "RESULT=1"
done

if [ "$RESULT" == "1" ];
    then
       exit 1
fi

...code continued here ...
patapouf_ai
fuente
1
Según las páginas del manual de espera, esperar con múltiples PID solo devuelve el valor de retorno del último proceso esperado. Por lo tanto, necesita un ciclo adicional y esperar cada PID por separado, como se sugiere en la respuesta aceptada (en los comentarios).
Vlad Frolov
1
Debido a que no parece estar indicado en ningún otro lugar de esta página, agregaré que el ciclo seríafor pid in $pids; do wait $pid; done
Bryan
1
@bisounours_tronconneuse sí, lo haces. Ver help wait: con múltiples ID waitdevuelve el código de salida del último solo, como @ vlad-frolov dijo anteriormente.
Sam Brightman
1
Bryan, @SamBrightman Ok. Lo modifiqué con tus recomendaciones.
patapouf_ai
44
Tenía una preocupación obvia con esta solución: ¿qué waitpasa si un proceso determinado sale antes de que se llame al correspondiente ? Resulta que esto no es un problema: si waiten un proceso que ya ha salido, waitsaldrá inmediatamente con el estado del proceso ya salido. (¡Gracias, bashautores!)
Daniel Griscom
39

Esto es lo que se me ocurrió hasta ahora. Me gustaría ver cómo interrumpir el comando de suspensión si un niño termina, para que uno no tenga que sintonizar WAITALL_DELAYsu uso.

waitall() { # PID...
  ## Wait for children to exit and indicate whether all exited with 0 status.
  local errors=0
  while :; do
    debug "Processes remaining: $*"
    for pid in "$@"; do
      shift
      if kill -0 "$pid" 2>/dev/null; then
        debug "$pid is still alive."
        set -- "$@" "$pid"
      elif wait "$pid"; then
        debug "$pid exited with zero exit status."
      else
        debug "$pid exited with non-zero exit status."
        ((++errors))
      fi
    done
    (("$#" > 0)) || break
    # TODO: how to interrupt this sleep when a child terminates?
    sleep ${WAITALL_DELAY:-1}
   done
  ((errors == 0))
}

debug() { echo "DEBUG: $*" >&2; }

pids=""
for t in 3 5 4; do 
  sleep "$t" &
  pids="$pids $!"
done
waitall $pids
Mark Edgar
fuente
Uno podría saltear ese WAITALL_DELAY o establecerlo muy bajo, ya que no se inician procesos dentro del ciclo, no creo que sea demasiado costoso.
Marian
21

Para paralelizar esto ...

for i in $(whatever_list) ; do
   do_something $i
done

Traducirlo a esto ...

for i in $(whatever_list) ; do echo $i ; done | ## execute in parallel...
   (
   export -f do_something ## export functions (if needed)
   export PATH ## export any variables that are required
   xargs -I{} --max-procs 0 bash -c ' ## process in batches...
      {
      echo "processing {}" ## optional
      do_something {}
      }' 
   )
  • Si se produce un error en un proceso, no interrumpirá los otros procesos, pero dará como resultado un código de salida distinto de cero de la secuencia en su conjunto .
  • Exportar funciones y variables puede o no ser necesario, en cualquier caso particular.
  • Puede establecer en --max-procsfunción de la cantidad de paralelismo que desee ( 0significa "todo a la vez").
  • GNU Parallel ofrece algunas características adicionales cuando se usa en lugar de xargs, pero no siempre se instala por defecto.
  • El forbucle no es estrictamente necesario en este ejemplo, ya echo $ique básicamente solo está regenerando la salida de $(whatever_list). Solo creo que el uso de la forpalabra clave hace que sea un poco más fácil ver lo que está sucediendo.
  • El manejo de cadenas Bash puede ser confuso: descubrí que el uso de comillas simples funciona mejor para ajustar scripts no triviales.
  • Puede interrumpir fácilmente toda la operación (usando ^ C o similar), a diferencia del enfoque más directo al paralelismo de Bash .

Aquí hay un ejemplo de trabajo simplificado ...

for i in {0..5} ; do echo $i ; done |xargs -I{} --max-procs 2 bash -c '
   {
   echo sleep {}
   sleep 2s
   }'
sin bar
fuente
7

No creo que sea posible con la funcionalidad integrada de Bash.

Usted puede recibir una notificación cuando un niño sale:

#!/bin/sh
set -o monitor        # enable script job control
trap 'echo "child died"' CHLD

Sin embargo, no hay una forma aparente de obtener el estado de salida del niño en el controlador de señal.

Obtener ese estado secundario suele ser el trabajo de la waitfamilia de funciones en las API POSIX de nivel inferior. Desafortunadamente, el soporte de Bash para eso es limitado: puede esperar un proceso secundario específico (y obtener su estado de salida) o puede esperar a todos ellos y siempre obtener un resultado 0.

Lo que parece imposible de hacer es el equivalente de waitpid(-1), que bloquea hasta que regrese cualquier proceso hijo.

Alnitak
fuente
7

Veo muchos buenos ejemplos enumerados aquí, también quería lanzar el mío.

#! /bin/bash

items="1 2 3 4 5 6"
pids=""

for item in $items; do
    sleep $item &
    pids+="$! "
done

for pid in $pids; do
    wait $pid
    if [ $? -eq 0 ]; then
        echo "SUCCESS - Job $pid exited with a status of $?"
    else
        echo "FAILED - Job $pid exited with a status of $?"
    fi
done

Utilizo algo muy similar para iniciar / detener servidores / servicios en paralelo y verificar cada estado de salida. Funciona muy bien para mi. ¡Espero que esto ayude a alguien!

Jason Slobotski
fuente
Cuando lo detengo con Ctrl + CI todavía veo procesos ejecutándose en segundo plano.
karsten
2
@karsten: este es un problema diferente. Suponiendo que está usando bash puede atrapar una condición de salida (incluyendo Ctrl + C) y tener la actual y todos los procesos hijos, sacrificados contrap "kill 0" EXIT
Phil
@Phil es correcto. Como se trata de procesos en segundo plano, eliminar el proceso padre simplemente deja en ejecución cualquier proceso hijo. Mi ejemplo no atrapa ninguna señal, que se puede agregar si es necesario, como Phil ha declarado.
Jason Slobotski
6

Esto es algo que uso:

#wait for jobs
for job in `jobs -p`; do wait ${job}; done
jplozier
fuente
5

El siguiente código esperará la finalización de todos los cálculos y devolverá el estado de salida 1 si falla alguno de los cálculos .

#!/bin/bash
for i in $(seq 0 9); do
   (doCalculations $i >&2 & wait %1; echo $?) &
done | grep -qv 0 && exit 1
errr
fuente
5

Simplemente almacene los resultados fuera del shell, por ejemplo, en un archivo.

#!/bin/bash
tmp=/tmp/results

: > $tmp  #clean the file

for i in `seq 0 9`; do
  (doCalculations $i; echo $i:$?>>$tmp)&
done      #iterate

wait      #wait until all ready

sort $tmp | grep -v ':0'  #... handle as required
estani
fuente
5

Aquí está mi versión que funciona para múltiples pids, registra advertencias si la ejecución lleva demasiado tiempo y detiene los subprocesos si la ejecución lleva más tiempo que un valor dado.

function WaitForTaskCompletion {
    local pids="${1}" # pids to wait for, separated by semi-colon
    local soft_max_time="${2}" # If execution takes longer than $soft_max_time seconds, will log a warning, unless $soft_max_time equals 0.
    local hard_max_time="${3}" # If execution takes longer than $hard_max_time seconds, will stop execution, unless $hard_max_time equals 0.
    local caller_name="${4}" # Who called this function
    local exit_on_error="${5:-false}" # Should the function exit program on subprocess errors       

    Logger "${FUNCNAME[0]} called by [$caller_name]."

    local soft_alert=0 # Does a soft alert need to be triggered, if yes, send an alert once 
    local log_ttime=0 # local time instance for comparaison

    local seconds_begin=$SECONDS # Seconds since the beginning of the script
    local exec_time=0 # Seconds since the beginning of this function

    local retval=0 # return value of monitored pid process
    local errorcount=0 # Number of pids that finished with errors

    local pidCount # number of given pids

    IFS=';' read -a pidsArray <<< "$pids"
    pidCount=${#pidsArray[@]}

    while [ ${#pidsArray[@]} -gt 0 ]; do
        newPidsArray=()
        for pid in "${pidsArray[@]}"; do
            if kill -0 $pid > /dev/null 2>&1; then
                newPidsArray+=($pid)
            else
                wait $pid
                result=$?
                if [ $result -ne 0 ]; then
                    errorcount=$((errorcount+1))
                    Logger "${FUNCNAME[0]} called by [$caller_name] finished monitoring [$pid] with exitcode [$result]."
                fi
            fi
        done

        ## Log a standby message every hour
        exec_time=$(($SECONDS - $seconds_begin))
        if [ $((($exec_time + 1) % 3600)) -eq 0 ]; then
            if [ $log_ttime -ne $exec_time ]; then
                log_ttime=$exec_time
                Logger "Current tasks still running with pids [${pidsArray[@]}]."
            fi
        fi

        if [ $exec_time -gt $soft_max_time ]; then
            if [ $soft_alert -eq 0 ] && [ $soft_max_time -ne 0 ]; then
                Logger "Max soft execution time exceeded for task [$caller_name] with pids [${pidsArray[@]}]."
                soft_alert=1
                SendAlert

            fi
            if [ $exec_time -gt $hard_max_time ] && [ $hard_max_time -ne 0 ]; then
                Logger "Max hard execution time exceeded for task [$caller_name] with pids [${pidsArray[@]}]. Stopping task execution."
                kill -SIGTERM $pid
                if [ $? == 0 ]; then
                    Logger "Task stopped successfully"
                else
                    errrorcount=$((errorcount+1))
                fi
            fi
        fi

        pidsArray=("${newPidsArray[@]}")
        sleep 1
    done

    Logger "${FUNCNAME[0]} ended for [$caller_name] using [$pidCount] subprocesses with [$errorcount] errors."
    if [ $exit_on_error == true ] && [ $errorcount -gt 0 ]; then
        Logger "Stopping execution."
        exit 1337
    else
        return $errorcount
    fi
}

# Just a plain stupid logging function to replace with yours
function Logger {
    local value="${1}"

    echo $value
}

Ejemplo, espere a que finalicen los tres procesos, registre una advertencia si la ejecución demora más de 5 segundos, detenga todos los procesos si la ejecución demora más de 120 segundos. No salga del programa por fallas.

function something {

    sleep 10 &
    pids="$!"
    sleep 12 &
    pids="$pids;$!"
    sleep 9 &
    pids="$pids;$!"

    WaitForTaskCompletion $pids 5 120 ${FUNCNAME[0]} false
}
# Launch the function
someting
Orsiris de Jong
fuente
4

Si tiene disponible bash 4.2 o posterior, lo siguiente podría serle útil. Utiliza matrices asociativas para almacenar nombres de tareas y su "código", así como nombres de tareas y sus pids. También he incorporado un método simple de limitación de velocidad que puede ser útil si sus tareas consumen mucho tiempo de CPU o E / S y desea limitar la cantidad de tareas simultáneas.

El script inicia todas las tareas en el primer bucle y consume los resultados en el segundo.

Esto es un poco exagerado para casos simples, pero permite cosas bastante ordenadas. Por ejemplo, uno puede almacenar mensajes de error para cada tarea en otra matriz asociativa e imprimirlos después de que todo se haya establecido.

#! /bin/bash

main () {
    local -A pids=()
    local -A tasks=([task1]="echo 1"
                    [task2]="echo 2"
                    [task3]="echo 3"
                    [task4]="false"
                    [task5]="echo 5"
                    [task6]="false")
    local max_concurrent_tasks=2

    for key in "${!tasks[@]}"; do
        while [ $(jobs 2>&1 | grep -c Running) -ge "$max_concurrent_tasks" ]; do
            sleep 1 # gnu sleep allows floating point here...
        done
        ${tasks[$key]} &
        pids+=(["$key"]="$!")
    done

    errors=0
    for key in "${!tasks[@]}"; do
        pid=${pids[$key]}
        local cur_ret=0
        if [ -z "$pid" ]; then
            echo "No Job ID known for the $key process" # should never happen
            cur_ret=1
        else
            wait $pid
            cur_ret=$?
        fi
        if [ "$cur_ret" -ne 0 ]; then
            errors=$(($errors + 1))
            echo "$key (${tasks[$key]}) failed."
        fi
    done

    return $errors
}

main
stefanct
fuente
4

Acabo de modificar una secuencia de comandos en segundo plano y paralelizar un proceso.

Experimenté un poco (en Solaris con bash y ksh) y descubrí que 'esperar' genera el estado de salida si no es cero, o una lista de trabajos que devuelven una salida distinta de cero cuando no se proporciona ningún argumento PID. P.ej

Golpetazo:

$ sleep 20 && exit 1 &
$ sleep 10 && exit 2 &
$ wait
[1]-  Exit 2                  sleep 20 && exit 2
[2]+  Exit 1                  sleep 10 && exit 1

Ksh:

$ sleep 20 && exit 1 &
$ sleep 10 && exit 2 &
$ wait
[1]+  Done(2)                  sleep 20 && exit 2
[2]+  Done(1)                  sleep 10 && exit 1

Este resultado se escribe en stderr, por lo que una solución simple para el ejemplo de OP podría ser:

#!/bin/bash

trap "rm -f /tmp/x.$$" EXIT

for i in `seq 0 9`; do
  doCalculations $i &
done

wait 2> /tmp/x.$$
if [ `wc -l /tmp/x.$$` -gt 0 ] ; then
  exit 1
fi

Mientras esto:

wait 2> >(wc -l)

también devolverá un conteo pero sin el archivo tmp. Esto también se puede usar de esta manera, por ejemplo:

wait 2> >(if [ `wc -l` -gt 0 ] ; then echo "ERROR"; fi)

Pero esto no es mucho más útil que el archivo tmp IMO. No pude encontrar una manera útil de evitar el archivo tmp y al mismo tiempo evitar ejecutar la "espera" en una subshell, que no funcionará en absoluto.

Tosh
fuente
3

He intentado esto y he combinado todas las mejores partes de los otros ejemplos aquí. Este script ejecutará la checkpidsfunción cuando salga cualquier proceso en segundo plano y generará el estado de salida sin recurrir al sondeo.

#!/bin/bash

set -o monitor

sleep 2 &
sleep 4 && exit 1 &
sleep 6 &

pids=`jobs -p`

checkpids() {
    for pid in $pids; do
        if kill -0 $pid 2>/dev/null; then
            echo $pid is still alive.
        elif wait $pid; then
            echo $pid exited with zero exit status.
        else
            echo $pid exited with non-zero exit status.
        fi
    done
    echo
}

trap checkpids CHLD

wait
michaelt
fuente
3
#!/bin/bash
set -m
for i in `seq 0 9`; do
  doCalculations $i &
done
while fg; do true; done
  • set -m le permite usar fg & bg en un script
  • fg, además de poner el último proceso en primer plano, tiene el mismo estado de salida que el proceso en primer plano
  • while fgdejará de repetirse cuando fgsalga con un estado de salida distinto de cero

desafortunadamente, esto no manejará el caso cuando un proceso en segundo plano salga con un estado de salida distinto de cero. (el bucle no terminará inmediatamente. esperará a que se completen los procesos anteriores).

Jayen
fuente
3

Ya hay muchas respuestas aquí, pero me sorprende que nadie parezca haber sugerido usar matrices ... Entonces, esto es lo que hice: esto podría ser útil para algunos en el futuro.

n=10 # run 10 jobs
c=0
PIDS=()

while true

    my_function_or_command &
    PID=$!
    echo "Launched job as PID=$PID"
    PIDS+=($PID)

    (( c+=1 ))

    # required to prevent any exit due to error
    # caused by additional commands run which you
    # may add when modifying this example
    true

do

    if (( c < n ))
    then
        continue
    else
        break
    fi
done 


# collect launched jobs

for pid in "${PIDS[@]}"
do
    wait $pid || echo "failed job PID=$pid"
done
usuario3728501
fuente
3

¡Esto funciona, debería ser tan bueno, si no mejor, que la respuesta de @ HoverHell!

#!/usr/bin/env bash

set -m # allow for job control
EXIT_CODE=0;  # exit code of overall script

function foo() {
     echo "CHLD exit code is $1"
     echo "CHLD pid is $2"
     echo $(jobs -l)

     for job in `jobs -p`; do
         echo "PID => ${job}"
         wait ${job} ||  echo "At least one test failed with exit code => $?" ; EXIT_CODE=1
     done
}

trap 'foo $? $$' CHLD

DIRN=$(dirname "$0");

commands=(
    "{ echo "foo" && exit 4; }"
    "{ echo "bar" && exit 3; }"
    "{ echo "baz" && exit 5; }"
)

clen=`expr "${#commands[@]}" - 1` # get length of commands - 1

for i in `seq 0 "$clen"`; do
    (echo "${commands[$i]}" | bash) &   # run the command via bash in subshell
    echo "$i ith command has been issued as a background job"
done

# wait for all to finish
wait;

echo "EXIT_CODE => $EXIT_CODE"
exit "$EXIT_CODE"

# end

y, por supuesto, he inmortalizado este script, en un proyecto NPM que le permite ejecutar comandos bash en paralelo, útil para probar:

https://github.com/ORESoftware/generic-subshell

Alexander Mills
fuente
trap $? $$parece conjunto de códigos de salida a 0 y el PID de cáscara del golpe de ejecución actual, cada vez que para mí
inetknght
¿Estás absolutamente seguro de eso? No estoy seguro si eso tiene sentido.
Alexander Mills
2

La trampa es tu amiga. Puede atrapar ERR en muchos sistemas. Puede atrapar EXIT, o en DEBUG para realizar una pieza de código después de cada comando.

Esto además de todas las señales estándar.

Paul Hodges
fuente
1
Por favor, ¿puede elaborar su respuesta con algunos ejemplos?
ϹοδεMεδιϲ
2
set -e
fail () {
    touch .failure
}
expect () {
    wait
    if [ -f .failure ]; then
        rm -f .failure
        exit 1
    fi
}

sleep 2 || fail &
sleep 2 && false || fail &
sleep 2 || fail
expect

La set -eparte superior hace que su script se detenga en caso de falla.

expectvolverá 1si falla cualquier subtrabajo.

Yajo
fuente
2

Exactamente para este propósito escribí una bashfunción llamada :for.

Nota : :forno solo conserva y devuelve el código de salida de la función anómala, sino que también termina todas las instancias en ejecución paralelas. Lo que podría no ser necesario en este caso.

#!/usr/bin/env bash

# Wait for pids to terminate. If one pid exits with
# a non zero exit code, send the TERM signal to all
# processes and retain that exit code
#
# usage:
# :wait 123 32
function :wait(){
    local pids=("$@")
    [ ${#pids} -eq 0 ] && return $?

    trap 'kill -INT "${pids[@]}" &>/dev/null || true; trap - INT' INT
    trap 'kill -TERM "${pids[@]}" &>/dev/null || true; trap - RETURN TERM' RETURN TERM

    for pid in "${pids[@]}"; do
        wait "${pid}" || return $?
    done

    trap - INT RETURN TERM
}

# Run a function in parallel for each argument.
# Stop all instances if one exits with a non zero
# exit code
#
# usage:
# :for func 1 2 3
#
# env:
# FOR_PARALLEL: Max functions running in parallel
function :for(){
    local f="${1}" && shift

    local i=0
    local pids=()
    for arg in "$@"; do
        ( ${f} "${arg}" ) &
        pids+=("$!")
        if [ ! -z ${FOR_PARALLEL+x} ]; then
            (( i=(i+1)%${FOR_PARALLEL} ))
            if (( i==0 )) ;then
                :wait "${pids[@]}" || return $?
                pids=()
            fi
        fi
    done && [ ${#pids} -eq 0 ] || :wait "${pids[@]}" || return $?
}

uso

for.sh:

#!/usr/bin/env bash
set -e

# import :for from gist: https://gist.github.com/Enteee/c8c11d46a95568be4d331ba58a702b62#file-for
# if you don't like curl imports, source the actual file here.
source <(curl -Ls https://gist.githubusercontent.com/Enteee/c8c11d46a95568be4d331ba58a702b62/raw/)

msg="You should see this three times"

:(){
  i="${1}" && shift

  echo "${msg}"

  sleep 1
  if   [ "$i" == "1" ]; then sleep 1
  elif [ "$i" == "2" ]; then false
  elif [ "$i" == "3" ]; then
    sleep 3
    echo "You should never see this"
  fi
} && :for : 1 2 3 || exit $?

echo "You should never see this"
$ ./for.sh; echo $?
You should see this three times
You should see this three times
You should see this three times
1

Referencias

Ente
fuente
1

Utilicé esto recientemente (gracias a Alnitak):

#!/bin/bash
# activate child monitoring
set -o monitor

# locking subprocess
(while true; do sleep 0.001; done) &
pid=$!

# count, and kill when all done
c=0
function kill_on_count() {
    # you could kill on whatever criterion you wish for
    # I just counted to simulate bash's wait with no args
    [ $c -eq 9 ] && kill $pid
    c=$((c+1))
    echo -n '.' # async feedback (but you don't know which one)
}
trap "kill_on_count" CHLD

function save_status() {
    local i=$1;
    local rc=$2;
    # do whatever, and here you know which one stopped
    # but remember, you're called from a subshell
    # so vars have their values at fork time
}

# care must be taken not to spawn more than one child per loop
# e.g don't use `seq 0 9` here!
for i in {0..9}; do
    (doCalculations $i; save_status $i $?) &
done

# wait for locking subprocess to be killed
wait $pid
echo

A partir de ahí, se puede extrapolar fácilmente y tener un activador (tocar un archivo, enviar una señal) y cambiar los criterios de conteo (contar archivos tocados o lo que sea) para responder a ese activador. O si solo desea 'cualquier' rc distinto de cero, simplemente elimine el bloqueo de save_status.

Lloeki
fuente
1

Necesitaba esto, pero el proceso de destino no era hijo del shell actual, en cuyo caso wait $PIDno funciona. En su lugar encontré la siguiente alternativa:

while [ -e /proc/$PID ]; do sleep 0.1 ; done

Eso se basa en la presencia de procfs , que pueden no estar disponibles (Mac no lo proporciona, por ejemplo). Entonces, para la portabilidad, podría usar esto en su lugar:

while ps -p $PID >/dev/null ; do sleep 0.1 ; done
troelskn
fuente
1

La captura de la señal CHLD puede no funcionar porque puede perder algunas señales si llegan simultáneamente.

#!/bin/bash

trap 'rm -f $tmpfile' EXIT

tmpfile=$(mktemp)

doCalculations() {
    echo start job $i...
    sleep $((RANDOM % 5)) 
    echo ...end job $i
    exit $((RANDOM % 10))
}

number_of_jobs=10

for i in $( seq 1 $number_of_jobs )
do
    ( trap "echo job$i : exit value : \$? >> $tmpfile" EXIT; doCalculations ) &
done

wait 

i=0
while read res; do
    echo "$res"
    let i++
done < "$tmpfile"

echo $i jobs done !!!
mug896
fuente
1

La solución para esperar varios subprocesos y salir cuando alguno de ellos sale con un código de estado distinto de cero es mediante 'wait -n'

#!/bin/bash
wait_for_pids()
{
    for (( i = 1; i <= $#; i++ )) do
        wait -n $@
        status=$?
        echo "received status: "$status
        if [ $status -ne 0 ] && [ $status -ne 127 ]; then
            exit 1
        fi
    done
}

sleep_for_10()
{
    sleep 10
    exit 10
}

sleep_for_20()
{
    sleep 20
}

sleep_for_10 &
pid1=$!

sleep_for_20 &
pid2=$!

wait_for_pids $pid2 $pid1

el código de estado '127' es para un proceso no existente, lo que significa que el niño podría haber salido.

vishnuitta
fuente
1

Espere todos los trabajos y devuelva el código de salida del último trabajo fallido. A diferencia de las soluciones anteriores, esto no requiere guardar pid. Solo bg lejos, y espera.

function wait_ex {
    # this waits for all jobs and returns the exit code of the last failing job
    ecode=0
    while true; do
        wait -n
        err="$?"
        [ "$err" == "127" ] && break
        [ "$err" != "0" ] && ecode="$err"
    done
    return $ecode
}
Erik Aronesty
fuente
Esto funcionará y proporcionará de manera confiable el primer código de error de sus comandos ejecutados a menos que sea "comando no encontrado" (código 127).
Drevicko
0

Puede haber un caso en el que el proceso se complete antes de esperar el proceso. Si activamos esperar un proceso que ya ha finalizado, activará un error como pid no es un hijo de este shell. Para evitar tales casos, la siguiente función se puede utilizar para determinar si el proceso está completo o no:

isProcessComplete(){
PID=$1
while [ -e /proc/$PID ]
do
    echo "Process: $PID is still running"
    sleep 5
done
echo "Process $PID has finished"
}
Anju Prasannan
fuente
0

Creo que la forma más directa de ejecutar trabajos en paralelo y verificar el estado es utilizando archivos temporales. Ya hay un par de respuestas similares (por ejemplo, Nietzche-jou y mug896).

#!/bin/bash
rm -f fail
for i in `seq 0 9`; do
  doCalculations $i || touch fail &
done
wait 
! [ -f fail ]

El código anterior no es seguro para subprocesos. Si le preocupa que el código anterior se ejecute al mismo tiempo que él mismo, es mejor usar un nombre de archivo más exclusivo, como fail. $$. La última línea es cumplir el requisito: "devolver el código de salida 1 cuando cualquiera de los subprocesos termina con el código! = 0?" Lancé un requisito adicional allí para limpiar. Puede haber sido más claro escribirlo así:

#!/bin/bash
trap 'rm -f fail.$$' EXIT
for i in `seq 0 9`; do
  doCalculations $i || touch fail.$$ &
done
wait 
! [ -f fail.$$ ] 

Aquí hay un fragmento similar para recopilar resultados de múltiples trabajos: creo un directorio temporal, cuento los resultados de todas las subtareas en un archivo separado y luego los vuelco para su revisión. Esto realmente no coincide con la pregunta: lo estoy agregando como un bono:

#!/bin/bash
trap 'rm -fr $WORK' EXIT

WORK=/tmp/$$.work
mkdir -p $WORK
cd $WORK

for i in `seq 0 9`; do
  doCalculations $i >$i.result &
done
wait 
grep $ *  # display the results with filenames and contents
marca
fuente
0

Casi caigo en la trampa de usar jobs -ppara recolectar PID, que no funciona si el niño ya ha salido, como se muestra en el script a continuación. La solución que elegí fue simplemente llamar wait -nN veces, donde N es el número de hijos que tengo, lo que resulta que sé determinista.

#!/usr/bin/env bash

sleeper() {
    echo "Sleeper $1"
    sleep $2
    echo "Exiting $1"
    return $3
}

start_sleepers() {
    sleeper 1 1 0 &
    sleeper 2 2 $1 &
    sleeper 3 5 0 &
    sleeper 4 6 0 &
    sleep 4
}

echo "Using jobs"
start_sleepers 1

pids=( $(jobs -p) )

echo "PIDS: ${pids[*]}"

for pid in "${pids[@]}"; do
    wait "$pid"
    echo "Exit code $?"
done

echo "Clearing other children"
wait -n; echo "Exit code $?"
wait -n; echo "Exit code $?"

echo "Waiting for N processes"
start_sleepers 2

for ignored in $(seq 1 4); do
    wait -n
    echo "Exit code $?"
done

Salida:

Using jobs
Sleeper 1
Sleeper 2
Sleeper 3
Sleeper 4
Exiting 1
Exiting 2
PIDS: 56496 56497
Exiting 3
Exit code 0
Exiting 4
Exit code 0
Clearing other children
Exit code 0
Exit code 1
Waiting for N processes
Sleeper 1
Sleeper 2
Sleeper 3
Sleeper 4
Exiting 1
Exiting 2
Exit code 0
Exit code 2
Exiting 3
Exit code 0
Exiting 4
Exit code 0
Daniel C. Sobral
fuente