Comment attendre en bash que plusieurs sous-processus se terminent et retourner le code de sortie! = 0 lorsqu'un sous-processus se termine par le code! = 0?

563

Comment attendre dans un script bash que plusieurs sous-processus générés à partir de ce script se terminent et retournent le code de sortie! = 0 lorsqu'un des sous-processus se termine par le code! = 0?

Script simple:

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

Le script ci-dessus attendra les 10 sous-processus générés, mais il donnera toujours le statut de sortie 0 (voir help wait). Comment puis-je modifier ce script pour qu'il découvre les états de sortie des sous-processus générés et renvoie le code de sortie 1 lorsque l'un des sous-processus se termine par le code! = 0?

Existe-t-il une meilleure solution pour cela que de collecter les PID des sous-processus, de les attendre dans l'ordre et de additionner les états de sortie?

tkokoszka
la source
1
Cela pourrait être considérablement amélioré au toucher wait -n, disponible dans bash moderne pour revenir uniquement lorsque la première / prochaine commande est terminée.
Charles Duffy
si vous cherchez à tester avec Bash, essayez ceci: github.com/sstephenson/bats
Alexander Mills
2
Le développement actif de BATS a migré
Potherca
3
@CharlesDuffy wait -na un petit problème: s'il ne reste aucun travail enfant (alias condition de concurrence ), il renvoie un état de sortie différent de zéro (échec) qui peut être distingué d'un processus enfant échoué.
drevicko
5
@CharlesDuffy - Vous avez une merveilleuse perspicacité et vous rendez un énorme service à SO en la partageant. Il semble qu'environ 80% des messages SO que j'ai lus vous font partager de merveilleux petits diamants de connaissances dans les commentaires qui doivent provenir d'un vaste océan d'expérience. Merci beaucoup!
Brett Holman

Réponses:

521

waitprend (facultativement) le PID du processus à attendre, et avec $! vous obtenez le PID de la dernière commande lancée en arrière-plan. Modifiez la boucle pour stocker le PID de chaque sous-processus généré dans un tableau, puis bouclez à nouveau en attente sur chaque 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
la source
9
Weel, puisque vous allez attendre tous les processus, peu importe si par exemple vous attendez sur le premier alors que le second est déjà terminé (le 2ème sera de toute façon choisi à la prochaine itération). C'est la même approche que vous utiliseriez en C avec wait (2).
Luca Tettamanti
7
Ah, je vois - interprétation différente :) J'ai lu la question comme signifiant "retourner le code de sortie 1 immédiatement lorsque l'un des sous-processus se termine".
Alnitak
56
Le PID peut être réutilisé en effet, mais vous ne pouvez pas attendre un processus qui n'est pas un enfant du processus actuel (l'attente échoue dans ce cas).
tkokoszka
12
Vous pouvez également utiliser% n pour faire référence au n: ème travail en arrière-plan et %% pour faire référence au plus récent.
conny
30
@Nils_M: Vous avez raison, je suis désolé. Ce serait donc quelque chose comme: for i in $n_procs; do ./procs[${i}] & ; pids[${i}]=$!; done; for pid in ${pids[*]}; do wait $pid; done;non?
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
la source
104
jobs -pdonne des PID de sous-processus qui sont en état d'exécution. Il sautera un processus si le processus se termine avant jobs -pson appel. Donc, si l'un des sous-processus se termine avant jobs -p, l'état de sortie de ce processus sera perdu.
tkokoszka
15
Wow, cette réponse est bien meilleure que celle la mieux notée. : /
e40
4
@ e40 et la réponse ci-dessous est probablement encore meilleure. Et encore mieux serait probablement d'exécuter chaque commande avec '(cmd; echo "$?" >> "$ tmpfile"), utilisez cette attente, puis lisez le fichier pour les échecs. Annoter également la sortie. … Ou utilisez simplement ce script quand vous ne vous en souciez pas beaucoup.
HoverHell
Je voudrais ajouter que cette réponse est meilleure que celle acceptée
shurikk
2
Pour être précis, @tkokoszka jobs -pne donne pas de PID de sous-processus, mais plutôt des GPID . La logique d'attente semble fonctionner de toute façon, elle attend toujours le groupe si un tel groupe existe et pid sinon, mais il est bon d'être conscient .. surtout si l'on devait s'appuyer sur cela et incorporer quelque chose comme l'envoi de messages au sous-processus dans lequel cas, la syntaxe est différente selon que vous avez des PID ou des GPID .. ie kill -- -$GPIDvskill $PID
Timo
59

Voici un exemple simple d'utilisation wait.

Exécutez certains processus:

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

Attendez-les ensuite avec la waitcommande:

$ wait < <(jobs -p)

Ou tout simplement wait(sans arguments) pour tous.

Cela attendra que tous les travaux en arrière-plan soient terminés.

Si l' -noption est fournie, attend la fin du travail suivant et retourne son état de sortie.

Voir: help waitet help jobspour la syntaxe.

Cependant, l'inconvénient est que cela ne retournera que le statut du dernier ID, vous devez donc vérifier le statut de chaque sous-processus et le stocker dans la variable.

Ou faites votre fonction de calcul pour créer un fichier en cas d'échec (vide ou avec journal des échecs), puis vérifiez ce fichier s'il existe, par exemple

$ sleep 20 && true || tee fail &
$ sleep 20 && false || tee fail &
$ wait < <(jobs -p)
$ test -f fail && echo Calculation failed.
kenorb
la source
2
Pour ceux qui découvrent bash, les deux calculs dans l'exemple ici sont sleep 20 && trueet sleep 20 && false- c'est-à-dire: remplacez ceux par votre (vos) fonction (s). Pour comprendre &&et ||, exécutez man bashet tapez '/' (recherche) puis '^ * Listes' (une expression régulière) puis entrez: l'homme &&||
défilera
1
vous devriez probablement vérifier que le fichier «échec» n'existe pas au début (ou le supprimer). Selon l'application, il peut également être judicieux d'ajouter «2> & 1» avant ||de capturer STDERR en échec également.
drevicko
j'aime celui-ci, des inconvénients? en fait, uniquement lorsque je veux répertorier tous les sous-processus et prendre certaines mesures, par exemple. envoyer un signal, que j'essaierai de pids de comptabilité ou itérer les emplois. Attendez la fin, justewait
xgwang
Cela manquera le statut de sortie du travail qui a échoué avant que le travail -p soit appelé
Erik Aronesty
50

Si GNU Parallel est installé, vous pouvez faire:

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

GNU Parallel vous donnera le code de sortie:

  • 0 - Toutes les tâches ont été exécutées sans erreur.

  • 1-253 - Certains travaux ont échoué. L'état de sortie donne le nombre de travaux ayant échoué

  • 254 - Plus de 253 travaux ont échoué.

  • 255 - Autre erreur.

Regardez les vidéos d'introduction pour en savoir plus: http://pi.dk/1

Ole Tange
la source
1
Merci! Mais vous avez oublié de mentionner le problème de "confusion" dans lequel je suis tombé par la suite: unix.stackexchange.com/a/35953
nobar
1
Cela ressemble à un excellent outil, mais je ne pense pas que ce qui précède fonctionne tel quel dans un script Bash où doCalculationsest une fonction définie dans ce même script (bien que l'OP n'était pas clair sur cette exigence). Quand j'essaye, paralleldit /bin/bash: doCalculations: command not found(il le dit 10 fois pour l' seq 0 9exemple ci-dessus). Voir ici pour une solution de contournement.
nobar
3
Également intéressant: xargsa une certaine capacité de lancer des travaux en parallèle via l' -Poption. De : export -f doCalculations ; seq 0 9 |xargs -P 0 -n 1 -I{} bash -c "doCalculations {}". Les limitations de xargssont énumérées dans la page de manuel de parallel.
nobar
Et si elle doCalculationss'appuie sur d'autres variables d'environnement internes au script (personnalisées PATH, etc.), elles doivent probablement être éditées explicitement exportavant le lancement parallel.
nobar
4
@nobar La confusion est due à certains emballeurs qui gâchent les choses pour leurs utilisateurs. Si vous installez en utilisant wget -O - pi.dk/3 | shvous n'obtiendrez aucune confusion. Si votre emballeur a gâché les choses pour vous, je vous encourage à soulever le problème avec votre emballeur. Les variables et les fonctions doivent être exportées (export -f) pour GNU Parallel pour les voir (voir man parallel: gnu.org/software/parallel/… )
Ole Tange
46

Que diriez-vous simplement:

#!/bin/bash

pids=""

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

wait $pids

...code continued here ...

Mise à jour:

Comme l'ont souligné plusieurs commentateurs, ce qui précède attend que tous les processus soient terminés avant de continuer, mais ne se termine pas et échoue si l'un d'entre eux échoue, cela peut être fait avec la modification suivante suggérée par @Bryan, @SamBrightman et d'autres :

#!/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
la source
1
Selon les pages de manuel wait, wait avec plusieurs PID ne renvoie que la valeur de retour du dernier processus attendu. Vous avez donc besoin d'une boucle supplémentaire et attendez chaque PID séparément, comme suggéré dans la réponse acceptée (dans les commentaires).
Vlad Frolov
1
Parce qu'il ne semble pas être indiqué ailleurs sur cette page, j'ajouterai que la boucle seraitfor pid in $pids; do wait $pid; done
Bryan
1
@bisounours_tronconneuse oui, vous le faites. Voir help wait- avec plusieurs ID waitrenvoie le code de sortie du dernier uniquement, comme l'a dit @ vlad-frolov ci-dessus.
Sam Brightman
1
Bryan, @SamBrightman Ok. Je l'ai modifié avec vos recommandations.
patapouf_ai
4
J'avais une préoccupation évidente avec cette solution: que faire si un processus donné se termine avant l' waitappel du correspondant ? Il s'avère que ce n'est pas un problème: si vous waitsur un processus qui a déjà été quitté, vous quitterez waitimmédiatement avec le statut du processus déjà terminé. (Merci, bashauteurs!)
Daniel Griscom
39

Voici ce que j'ai trouvé jusqu'à présent. Je voudrais voir comment interrompre la commande de sommeil si un enfant se termine, afin que l'on n'ait pas à s'adapter WAITALL_DELAYà son utilisation.

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
la source
On pourrait peut-être ignorer ce WAITALL_DELAY ou le régler très bas, car aucun processus n'est démarré dans la boucle, je ne pense pas que ce soit trop cher.
Marian
21

Pour paralléliser cela ...

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

Traduisez-le en ceci ...

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 une erreur se produit dans un processus, elle n'interrompra pas les autres processus, mais elle entraînera un code de sortie non nul de la séquence dans son ensemble .
  • L'exportation de fonctions et de variables peut être nécessaire ou non, dans tous les cas particuliers.
  • Vous pouvez définir en --max-procsfonction de la quantité de parallélisme que vous souhaitez ( 0signifie «tout à la fois»).
  • GNU Parallel offre quelques fonctionnalités supplémentaires lorsqu'il est utilisé à la place de xargs- mais il n'est pas toujours installé par défaut.
  • La forboucle n'est pas strictement nécessaire dans cet exemple car il echo $is'agit simplement de régénérer la sortie de $(whatever_list). Je pense simplement que l'utilisation du formot - clé permet de voir un peu plus facilement ce qui se passe.
  • La gestion des chaînes Bash peut être déroutante - j'ai trouvé que l'utilisation de guillemets simples fonctionne mieux pour encapsuler des scripts non triviaux.
  • Vous pouvez facilement interrompre toute l'opération (en utilisant ^ C ou similaire), contrairement à l'approche plus directe du parallélisme Bash .

Voici un exemple de travail simplifié ...

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

Je ne pense pas que ce soit possible avec la fonctionnalité intégrée de Bash.

Vous pouvez recevoir une notification lorsqu'un enfant quitte:

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

Cependant, il n'y a aucun moyen apparent d'obtenir le statut de sortie de l'enfant dans le gestionnaire de signaux.

Obtenir ce statut enfant est généralement le travail de la waitfamille de fonctions dans les API POSIX de niveau inférieur. Malheureusement, le support de Bash est limité - vous pouvez attendre un processus enfant spécifique (et obtenir son statut de sortie) ou vous pouvez tous les attendre , et toujours obtenir un résultat 0.

Ce qu'il semble impossible de faire est l'équivalent de waitpid(-1), qui bloque jusqu'à ce que tout processus enfant revienne.

Alnitak
la source
7

Je vois beaucoup de bons exemples énumérés ici, je voulais aussi jeter le mien.

#! /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

J'utilise quelque chose de très similaire pour démarrer / arrêter des serveurs / services en parallèle et vérifier chaque état de sortie. Fonctionne très bien pour moi. J'espère que cela aide quelqu'un!

Jason Slobotski
la source
Lorsque je l'arrête avec Ctrl + CI, les processus s'exécutent toujours en arrière-plan.
Karsten
2
@karsten - c'est un problème différent. En supposant que vous utilisez bash, vous pouvez intercepter une condition de sortie (y compris Ctrl + C) et avoir le processus actuel et tous les processus enfants tués à l'aide detrap "kill 0" EXIT
Phil
@Phil est correct. Comme il s'agit de processus d'arrière-plan, la suppression du processus parent ne fait que laisser les processus enfants en cours d'exécution. Mon exemple ne piège aucun signal, qui peut être ajouté si nécessaire, comme l'a déclaré Phil.
Jason Slobotski
6

C'est quelque chose que j'utilise:

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

Le code suivant attend la fin de tous les calculs et renvoie l'état de sortie 1 en cas d' échec de doCalculations .

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

Il suffit de stocker les résultats hors du shell, par exemple dans un fichier.

#!/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
la source
5

Voici ma version qui fonctionne pour plusieurs pids, enregistre les avertissements si l'exécution prend trop de temps et arrête les sous-processus si l'exécution prend plus de temps qu'une valeur donnée.

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
}

Exemple, attendez la fin des trois processus, enregistrez un avertissement si l'exécution prend plus de 5 secondes, arrêtez tous les processus si l'exécution dure plus de 120 secondes. Ne quittez pas le programme en cas d'échecs.

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
la source
4

Si vous disposez de bash 4.2 ou version ultérieure, les éléments suivants peuvent vous être utiles. Il utilise des tableaux associatifs pour stocker les noms de tâches et leur "code" ainsi que les noms de tâches et leurs pids. J'ai également intégré une méthode de limitation de débit simple qui pourrait être utile si vos tâches consomment beaucoup de temps processeur ou d'E / S et que vous souhaitez limiter le nombre de tâches simultanées.

Le script lance toutes les tâches dans la première boucle et consomme les résultats dans la seconde.

C'est un peu exagéré pour les cas simples mais cela permet des trucs assez soignés. Par exemple, on peut stocker des messages d'erreur pour chaque tâche dans un autre tableau associatif et les imprimer une fois que tout est réglé.

#! /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
la source
4

Je viens de modifier un script en arrière-plan et de paralléliser un processus.

J'ai fait quelques essais (sur Solaris avec bash et ksh) et j'ai découvert que `` wait '' sort l'état de sortie s'il n'est pas nul, ou une liste de travaux qui retournent une sortie non nulle quand aucun argument PID n'est fourni. Par exemple

Frapper:

$ 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

Cette sortie est écrite dans stderr, donc une solution simple à l'exemple OP pourrait être:

#!/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

Alors que ce:

wait 2> >(wc -l)

renverra également un nombre, mais sans le fichier tmp. Cela peut également être utilisé de cette façon, par exemple:

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

Mais ce n'est pas beaucoup plus utile que le fichier tmp IMO. Je ne pouvais pas trouver un moyen utile d'éviter le fichier tmp tout en évitant d'exécuter l '"attente" dans un sous-shell, qui ne fonctionnera pas du tout.

Tosh
la source
3

J'ai essayé et j'ai combiné toutes les meilleures parties des autres exemples ici. Ce script exécutera la checkpidsfonction à la fin d' un processus d'arrière-plan et affichera l'état de sortie sans recourir à l'interrogation.

#!/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
la source
3
#!/bin/bash
set -m
for i in `seq 0 9`; do
  doCalculations $i &
done
while fg; do true; done
  • set -m vous permet d'utiliser fg & bg dans un script
  • fg, en plus de mettre le dernier processus au premier plan, a le même statut de sortie que le processus qu'il met en avant
  • while fgarrêtera la boucle lorsque des fgsorties avec un état de sortie non nul

malheureusement, cela ne traitera pas le cas lorsqu'un processus en arrière-plan se termine avec un état de sortie non nul. (la boucle ne se terminera pas immédiatement. elle attendra la fin des processus précédents.)

Jayen
la source
3

Il y a déjà beaucoup de réponses ici, mais je suis surpris que personne ne semble avoir suggéré d'utiliser des tableaux ... Alors voici ce que j'ai fait - cela pourrait être utile à certains à l'avenir.

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
user3728501
la source
3

Cela fonctionne, devrait être aussi bon sinon meilleur que la réponse 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

et bien sûr, j'ai immortalisé ce script, dans un projet NPM qui vous permet d'exécuter des commandes bash en parallèle, utile pour tester:

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

Alexander Mills
la source
trap $? $$semble mettre le code de sortie à 0 et le PID au shell bash en cours d'exécution, à chaque fois pour moi
inetknght
vous en êtes absolument sûr? Je ne sais pas si cela a du sens.
Alexander Mills
2

piège est votre ami. Vous pouvez intercepter ERR dans de nombreux systèmes. Vous pouvez intercepter EXIT ou DEBUG pour exécuter un morceau de code après chaque commande.

Ceci en plus de tous les signaux standard.

Paul Hodges
la source
1
Veuillez élaborer votre réponse avec quelques exemples.
Ϲοδε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

En set -ehaut, votre script s'arrête en cas d'échec.

expectretournera 1si un sous-job a échoué.

Yajo
la source
2

Exactement à cet effet, j'ai écrit une bashfonction appelée :for.

Remarque : :fornon seulement conserve et renvoie le code de sortie de la fonction défaillante, mais met également fin à toutes les instances en cours d'exécution en parallèle. Ce qui pourrait ne pas être nécessaire dans ce cas.

#!/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 $?
}

usage

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

Références

Ente
la source
1

Je l'ai utilisé récemment (grâce à 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

De là, on peut facilement extrapoler, et avoir un déclencheur (toucher un fichier, envoyer un signal) et changer les critères de comptage (compter les fichiers touchés, ou autre) pour répondre à ce déclencheur. Ou si vous voulez juste 'any' rc non nul, tuez simplement le verrou de save_status.

Lloeki
la source
1

J'en avais besoin, mais le processus cible n'était pas un enfant du shell actuel, auquel cas wait $PIDcela ne fonctionne pas. J'ai trouvé l'alternative suivante à la place:

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

Cela repose sur la présence de procfs , qui peuvent ne pas être disponibles (Mac ne le fournit pas par exemple). Donc, pour la portabilité, vous pouvez utiliser ceci à la place:

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

Le piégeage du signal CHLD peut ne pas fonctionner car vous pouvez perdre certains signaux s'ils arrivent simultanément.

#!/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
la source
1

solution d'attendre plusieurs sous-processus et de quitter lorsque l'un d'eux se termine avec un code d'état différent de zéro en utilisant '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

le code d'état «127» est pour un processus inexistant, ce qui signifie que l'enfant peut être sorti.

vishnuitta
la source
1

Attendez tous les travaux et renvoyez le code de sortie du dernier travail ayant échoué. Contrairement aux solutions ci-dessus, cela ne nécessite pas de sauvegarde pid. Juste bg loin, et attendez.

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
la source
Cela fonctionnera et donnera de manière fiable le premier code d'erreur de vos commandes exécutées à moins qu'il ne s'agisse de "commande introuvable" (code 127).
drevicko
0

Il peut y avoir un cas où le processus est terminé avant d'attendre le processus. Si nous déclenchons l'attente d'un processus qui est déjà terminé, cela déclenchera une erreur comme pid n'est pas un enfant de ce shell. Pour éviter de tels cas, la fonction suivante peut être utilisée pour déterminer si le processus est terminé ou non:

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

Je pense que la façon la plus simple d'exécuter des travaux en parallèle et de vérifier l'état consiste à utiliser des fichiers temporaires. Il existe déjà quelques réponses similaires (par exemple Nietzche-jou et mug896).

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

Le code ci-dessus n'est pas sûr pour les threads. Si vous craignez que le code ci-dessus ne s'exécute en même temps que lui-même, il est préférable d'utiliser un nom de fichier plus unique, comme fail. $$. La dernière ligne doit répondre à l'exigence: "retourner le code de sortie 1 lorsque l'un des sous-processus se termine par le code! = 0?" J'ai jeté une exigence supplémentaire pour nettoyer. Il aurait peut-être été plus clair de l'écrire comme ceci:

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

Voici un extrait similaire pour rassembler les résultats de plusieurs travaux: je crée un répertoire temporaire, j'expose les sorties de toutes les sous-tâches dans un fichier séparé, puis je les vide pour examen. Cela ne correspond pas vraiment à la question - je le jette en bonus:

#!/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
marque
la source
0

Je suis presque tombé dans le piège de l'utilisation jobs -ppour collecter des PID, ce qui ne fonctionne pas si l'enfant est déjà sorti, comme indiqué dans le script ci-dessous. La solution que j'ai choisie consistait simplement à appeler wait -nN fois, où N est le nombre d'enfants que j'ai, que je connais de façon déterministe.

#!/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

Production:

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
la source