Como aguardar no bash vários subprocessos para concluir e retornar o código de saída! = 0 quando qualquer subprocesso termina com o código! = 0?


562

Como aguardar em um script bash por vários subprocessos gerados nesse script para concluir e retornar o código de saída! = 0 quando qualquer um dos subprocessos termina com o código! = 0?

Script simples:

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

O script acima aguardará todos os 10 subprocessos gerados, mas sempre dará o status de saída 0 (consulte help wait). Como posso modificar esse script para descobrir os status de saída dos subprocessos gerados e retornar o código de saída 1 quando qualquer um dos subprocessos termina com o código! = 0?

Existe alguma solução melhor para isso do que coletar PIDs dos subprocessos, aguardá-los em ordem e somar os status de saída?


1
Isso pode ser significativamente aprimorado ao toque wait -n, disponível no bash moderno para retornar somente quando o primeiro / próximo comando for concluído.
Charles Duffy

se você estiver procurando testar usando o Bash, tente o seguinte: github.com/sstephenson/bats
Alexander Mills

2
Desenvolvimento ativo da BATS se mudaram para github.com/bats-core/bats-core
Potherca

3
O @CharlesDuffy wait -ntem um pequeno problema: se não houver trabalhos filhos restantes (condição de corrida), ele retornará um status de saída diferente de zero (falha) que pode ser indistinguível de um processo filho com falha.
drevicko

5
@CharlesDuffy - Você tem uma visão maravilhosa e presta um serviço enorme para SO compartilhando-a. Parece que cerca de 80% das postagens de SO que li têm compartilhando maravilhosos pequenos diamantes de conhecimento nos comentários que devem vir de um vasto oceano de experiência. Muito Obrigado!
Brett Holman

Respostas:


520

waittambém (opcionalmente) leva o PID do processo para aguardar e com $! você obtém o PID do último comando lançado em segundo plano. Modifique o loop para armazenar o PID de cada subprocesso gerado em uma matriz e, em seguida, faça o loop novamente esperando em 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

9
Bem, desde que você esperará por todos os processos, não importa se, por exemplo, você está aguardando o primeiro enquanto o segundo já terminou (o segundo será escolhido na próxima iteração). É a mesma abordagem que você usaria em C com wait (2).
Luca Tettamanti

7
Ah, entendo - interpretação diferente :) Eu li a pergunta como significando "retornar código de saída 1 imediatamente quando qualquer um dos subprocessos sair".
Alnitak

56
O PID pode realmente ser reutilizado, mas você não pode esperar por um processo que não é filho do processo atual (a espera falhar nesse caso).
Tcposzka

12
Você também pode usar% n para se referir ao n: ésimo trabalho em segundo plano e %% para se referir ao mais recente.
conny

30
@ Nils_M: Você está certo, me desculpe. Então seria algo como:, for i in $n_procs; do ./procs[${i}] & ; pids[${i}]=$!; done; for pid in ${pids[*]}; do wait $pid; done;certo?
synack 27/05

284

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

103
jobs -pestá fornecendo PIDs de subprocessos que estão no estado de execução. Irá pular um processo se o processo terminar antes de jobs -pser chamado. Portanto, se algum subprocesso terminar antes jobs -p, o status de saída desse processo será perdido.
tkokoszka 8/02/09

15
Uau, esta resposta é muito melhor do que a mais votada. : /
e40

4
@ e40 e a resposta abaixo é provavelmente ainda melhor. E o melhor seria provavelmente executar cada comando com '(cmd; echo "$?" >> "$ tmpfile"), use essa espera e leia o arquivo para ver se há falhas. Anote também a saída. ... ou apenas use esse script quando você não se importa muito.
HoverHell 29/03/2012

Eu gostaria de acrescentar que essa resposta é melhor do que a resposta aceita #
shurikk

2
@tkokoszka, para ser mais preciso, jobs -pnão fornece PIDs de subprocessos, mas GPIDs . A lógica de espera parece funcionar de qualquer maneira, sempre espera no grupo se esse grupo existe e é ridículo se não, mas é bom estar ciente .. especialmente se alguém se basear nisso e incorporar algo como enviar mensagens ao subprocesso no qual caso a sintaxe seja diferente dependendo se você tem PIDs ou GPIDs. ie kill -- -$GPIDvskill $PID
Timo

58

Aqui está um exemplo simples usando wait.

Execute alguns processos:

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

Então espere por eles com o waitcomando:

$ wait < <(jobs -p)

Ou apenas wait(sem argumentos) para todos.

Isso aguardará a conclusão de todos os trabalhos em segundo plano.

Se a -nopção for fornecida, aguarda o término do próximo trabalho e retorna seu status de saída.

Veja: help waite help jobspara sintaxe.

No entanto, a desvantagem é que isso retornará apenas o status do último ID, portanto, é necessário verificar o status de cada subprocesso e armazená-lo na variável.

Ou faça sua função de cálculo para criar algum arquivo com falha (vazio ou com log de falhas) e verifique esse arquivo, se existir, por exemplo

$ sleep 20 && true || tee fail &
$ sleep 20 && false || tee fail &
$ wait < <(jobs -p)
$ test -f fail && echo Calculation failed.

1
Para os iniciantes no bash, os dois cálculos no exemplo aqui são sleep 20 && truee sleep 20 && false- ie: substitua-os pelas suas funções. Para entender &&e ||, execute man bashe digite '/' (pesquisa) e '^ * Listas' (um regex) e digite: man rolará para baixo até a descrição de &&e||
drevicko

1
você provavelmente deve verificar se o arquivo 'falhar' não existe no início (ou excluí-lo). Dependendo do aplicativo, também pode ser uma boa ideia adicionar '2> & 1' antes que o ||STDERR também falhe.
drevicko

Eu gosto deste, alguma desvantagem? na verdade, somente quando eu quero listar todos os subprocessos e executar algumas ações, por exemplo. enviar sinal, que tentarei contabilizar pids ou repetir trabalhos. Espere por fim, apenaswait
xgwang

Isso perderá o status de saída do trabalho que falhou antes dos trabalhos -p ser chamado
Erik Aronesty

50

Se você possui o GNU Parallel instalado, você pode:

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

O GNU Parallel fornecerá o código de saída:

  • 0 - Todos os trabalhos foram executados sem erro.

  • 1-253 - Alguns dos trabalhos falharam. O status de saída fornece o número de tarefas com falha

  • 254 - Mais de 253 trabalhos falharam.

  • 255 - Outro erro.

Assista aos vídeos de introdução para saber mais: http://pi.dk/1


1
Obrigado! Mas você esqueceu de mencionar a questão da "confusão" na qual caí posteriormente: unix.stackexchange.com/a/35953
nobar

1
Isso parece uma ótima ferramenta, mas não acho que o acima funcione como está em um script Bash, onde doCalculationsé uma função definida no mesmo script (embora o OP não tenha sido claro sobre esse requisito). Quando tento, paralleldiz /bin/bash: doCalculations: command not found(diz isso 10 vezes para o seq 0 9exemplo acima). Veja aqui uma solução alternativa.
No28

3
Também é interessante: xargspossui alguma capacidade para iniciar trabalhos em paralelo por meio da -Popção A partir daqui : export -f doCalculations ; seq 0 9 |xargs -P 0 -n 1 -I{} bash -c "doCalculations {}". Limitações de xargssão enumeradas na página de manual para parallel.
No28

E se doCalculationsdepender de outras variáveis ​​de ambiente internas do script (personalizadas PATH, etc.), elas provavelmente precisam ser explicitamente exporteditadas antes do lançamento parallel.
Nobar

4
@nobar A confusão se deve a alguns empacotadores bagunçando as coisas para seus usuários. Se você instalar usando wget -O - pi.dk/3 | shvocê não terá confusões. Se o seu empacotador estragou tudo para você, incentivo-o a levantar o problema com ele. Variáveis e funções devem ser exportados (-f exportação) para o GNU Paralela para vê-los (ver man parallel: gnu.org/software/parallel/... )
Ole Tange

46

Que tal simplesmente:

#!/bin/bash

pids=""

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

wait $pids

...code continued here ...

Atualizar:

Conforme apontado por vários comentaristas, o item acima espera que todos os processos sejam concluídos antes de continuar, mas não sai e falha se um deles falhar; isso pode ser feito com a seguinte modificação sugerida por @Bryan, @SamBrightman e outros :

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

1
De acordo com as páginas do manual wait, a espera com vários PIDs retorna apenas o valor de retorno do último processo esperado. Portanto, você precisa de um loop extra e aguarde cada PID separadamente, conforme sugerido na resposta aceita (nos comentários).
Vlad Frolov

1
Porque não parece ser indicado em qualquer outro lugar nesta página, vou acrescentar que o loop seriafor pid in $pids; do wait $pid; done
Bryan

1
@bisounours_tronconneuse sim, você faz. Veja help wait- com vários IDs waitretorna apenas o código de saída do último, como @ vlad-frolov disse acima.
Sam Brightman

1
Bryan, @SamBrightman Ok. Eu o modifiquei com suas recomendações.
patapouf_ai

4
Eu tinha uma preocupação óbvia com esta solução: e se um determinado processo sair antes que o correspondente waitseja chamado? Acontece que isso não é um problema: se você waitem um processo que já saiu, waitsairá imediatamente com o status do processo já encerrado. (Obrigada, bashautores!)
Daniel Griscom

39

Aqui está o que eu criei até agora. Gostaria de ver como interromper o comando de suspensão se uma criança terminar, para que não seja necessário ajustar WAITALL_DELAYo 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

É possível pular esse WAITALL_DELAY ou defini-lo como muito baixo, pois nenhum processo é iniciado dentro do loop. Não acho que seja muito caro.
Marian

21

Para paralelizar isso ...

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

Traduzir para este ...

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 {}
      }' 
   )
  • Se ocorrer um erro em um processo, ele não interromperá os outros processos, mas resultará em um código de saída diferente de zero da sequência como um todo .
  • Funções e variáveis ​​de exportação podem ou não ser necessárias, em qualquer caso particular.
  • Você pode definir com --max-procsbase na quantidade de paralelismo que deseja ( 0significa "de uma só vez").
  • O GNU Parallel oferece alguns recursos adicionais quando usado no lugar de xargs- mas nem sempre é instalado por padrão.
  • O forloop não é estritamente necessário neste exemplo, pois echo $ibasicamente regenera apenas a saída de $(whatever_list). Eu só acho que o uso dofor palavra-chave facilita um pouco a visualização do que está acontecendo.
  • O manuseio de string Bash pode ser confuso - descobri que usar aspas simples funciona melhor para agrupar scripts não triviais.
  • É possível interromper facilmente toda a operação (usando ^ C ou similar), diferente da abordagem mais direta ao paralelismo do Bash .

Aqui está um exemplo de trabalho simplificado ...

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


7

Não acredito que seja possível com a funcionalidade incorporada do Bash.

Você pode receber uma notificação quando um filho sair:

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

No entanto, não há uma maneira aparente de obter o status de saída da criança no manipulador de sinais.

Obter esse status filho geralmente é o trabalho da waitfamília de funções nas APIs POSIX de nível inferior. Infelizmente, o suporte do Bash é limitado - você pode esperar por um processo filho específico (e obter seu status de saída) ou pode esperar por todos eles e sempre obter um resultado 0.

O que parece impossível é o equivalente a waitpid(-1), que bloqueia até o retorno de qualquer processo filho.


7

Vejo muitos bons exemplos listados aqui, que também queriam incluir os meus.

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

Uso algo muito semelhante para iniciar / parar servidores / serviços em paralelo e verificar cada status de saída. Funciona muito bem para mim. Espero que isso ajude alguém!


Quando o interrompo com Ctrl + CI, ainda vejo processos em execução em segundo plano.
30818 karsten

2
@karsten - este é um problema diferente. Supondo que você estiver usando bash você pode prender uma condição de saída (incluindo Ctrl + C) e ter o atual e todos os processos de criança morta usandotrap "kill 0" EXIT
Phil

@ Phil está correto. Como esses são processos em segundo plano, eliminar o processo pai apenas deixa os processos filhos em execução. Meu exemplo não captura nenhum sinal que possa ser adicionado, se necessário, como Phil afirmou.
Jason Slobotski 11/07/19

6

Isso é algo que eu uso:

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

5

O código a seguir aguardará a conclusão de todos os cálculos e retornará o status de saída 1 se algum dos doCalculations falhar.

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

5

Apenas armazene os resultados fora do shell, por exemplo, em um arquivo.

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

5

Aqui está minha versão que funciona para vários pids, registra avisos se a execução demorar muito e interrompe os subprocessos se a execução demorar mais do que um determinado valor.

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
}

Por exemplo, aguarde a conclusão de todos os três processos, registre um aviso se a execução demorar mais de 5 segundos, interrompa todos os processos se a execução demorar mais de 120 segundos. Não saia do programa com falhas.

function something {

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

    WaitForTaskCompletion $pids 5 120 ${FUNCNAME[0]} false
}
# Launch the function
someting

4

Se você possui o bash 4.2 ou posterior, o seguinte pode ser útil para você. Ele usa matrizes associativas para armazenar nomes de tarefas e seus "códigos", assim como nomes de tarefas e seus pids. Também construí um método simples de limitação de taxa que pode ser útil se suas tarefas consumirem muito tempo de CPU ou E / S e você quiser limitar o número de tarefas simultâneas.

O script inicia todas as tarefas no primeiro loop e consome os resultados no segundo.

Isso é um pouco exagerado para casos simples, mas permite coisas bem legais. Por exemplo, pode-se armazenar mensagens de erro para cada tarefa em outra matriz associativa e imprimi-las depois que tudo se acalmar.

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

4

Acabei de modificar um script para segundo plano e paralelizar um processo.

Fiz algumas experiências (no Solaris com bash e ksh) e descobri que 'wait' gera o status de saída, se não for zero, ou uma lista de tarefas que retornam saídas diferentes de zero quando nenhum argumento PID é fornecido. Por exemplo

Bater:

$ 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

Esta saída é gravada no stderr, portanto, uma solução simples para o exemplo dos OPs pode 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

Enquanto isso:

wait 2> >(wc -l)

também retornará uma contagem, mas sem o arquivo tmp. Isso também pode ser usado dessa maneira, por exemplo:

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

Mas isso não é muito mais útil que o arquivo tmp IMO. Não consegui encontrar uma maneira útil de evitar o arquivo tmp, além de evitar executar a "espera" em um subshell, o que geralmente não funciona.


3

Eu experimentei isso e combinei todas as melhores partes dos outros exemplos aqui. Esse script executará a checkpidsfunção quando qualquer processo em segundo plano sair e produzirá o status de saída sem recorrer à pesquisa.

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

3
#!/bin/bash
set -m
for i in `seq 0 9`; do
  doCalculations $i &
done
while fg; do true; done
  • set -m permite que você use fg & bg em um script
  • fg, além de colocar o último processo em primeiro plano, tem o mesmo status de saída do processo em primeiro plano
  • while fginterromperá o loop quando houver fgsaídas com status de saída diferente de zero

infelizmente, isso não resolverá o caso quando um processo em segundo plano sair com um status de saída diferente de zero. (o loop não será encerrado imediatamente. aguardará a conclusão dos processos anteriores.)


3

Já existem muitas respostas aqui, mas estou surpreso que ninguém parece ter sugerido o uso de matrizes ... Então, aqui está o que eu fiz - isso pode ser útil para alguns no 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

3

Isso funciona, deve ser tão bom quanto não melhor que a resposta da @ 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

e, é claro, imortalizei esse script, em um projeto NPM que permite executar comandos bash em paralelo, úteis para testar:

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


trap $? $$parece set código de saída a 0 e PID para shell atual em execução bash, cada vez para mim
inetknght

você tem certeza absoluta disso? Não tenho certeza se isso faz sentido.
Alexander Mills

2

armadilha é sua amiga. Você pode interceptar o ERR em muitos sistemas. Você pode interceptar EXIT ou DEBUG para executar um pedaço de código após cada comando.

Isso além de todos os sinais padrão.


1
Por favor, você pode elaborar sua resposta com alguns exemplos.
ϲοδε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

o set -e parte superior faz com que seu script pare em falha.

expectretornará 1se algum subjob falhar.


2

Exatamente para esse propósito, escrevi uma bashfunção chamada :for.

Nota : :fornão apenas preserva e retorna o código de saída da função com falha, mas também finaliza toda a instância em execução paralela. O que pode não ser necessário neste 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

Referências


1

Eu usei isso recentemente (graças 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 daí, é possível extrapolar facilmente e acionar (tocar em um arquivo, enviar um sinal) e alterar os critérios de contagem (contagem de arquivos tocados ou o que for) para responder a esse disparador. Ou se você quiser apenas 'any' diferente de zero rc, basta matar o bloqueio de save_status.


1

Eu precisava disso, mas o processo de destino não era um filho do shell atual; nesse caso wait $PID, não funciona. Eu encontrei a seguinte alternativa:

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

Isso depende da presença de procfs , que pode não estar disponível (o Mac não fornece, por exemplo). Portanto, para portabilidade, você pode usar isso:

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

1

A captura do sinal CHLD pode não funcionar porque você pode perder alguns sinais se eles chegarem simultaneamente.

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

1

A solução para aguardar vários subprocessos e sair quando qualquer um deles sai com código de status diferente de zero é usando '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

o código de status '127' é para processo inexistente, o que significa que o filho pode ter saído.


1

Aguarde todos os trabalhos e retorne o código de saída do último trabalho com falha. Diferentemente das soluções acima, isso não requer economia de pid. Apenas vá embora e espere.

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
}

Isso funcionará e fornecerá com segurança o primeiro código de erro dos comandos executados, a menos que seja "comando não encontrado" (código 127).
drevicko 14/01

0

Pode haver um caso em que o processo esteja concluído antes de aguardar o processo. Se acionarmos a espera por um processo que já está concluído, ele causará um erro como o pid não é filho desse shell. Para evitar esses casos, a seguinte função pode ser usada para descobrir se o processo está completo ou não:

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

0

Eu acho que a maneira mais direta de executar tarefas em paralelo e verificar o status é usando arquivos temporários. Já existem algumas respostas semelhantes (por exemplo, Nietzche-jou e mug896).

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

O código acima não é seguro para threads. Se você está preocupado com o fato de o código acima estar em execução ao mesmo tempo, é melhor usar um nome de arquivo mais exclusivo, como fail. $$. A última linha é atender ao requisito: "retornar código de saída 1 quando qualquer um dos subprocessos terminar com o código! = 0?" Eu joguei um requisito extra lá para limpar. Pode ter sido mais claro escrevê-lo assim:

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

Aqui está um trecho semelhante para reunir resultados de vários trabalhos: eu crio um diretório temporário, descrevo as saídas de todas as subtarefas em um arquivo separado e as despejo para revisão. Isso realmente não corresponde à pergunta - estou jogando isso como um bônus:

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

0

Eu quase caí na armadilha de usar jobs -ppara coletar PIDs, o que não funciona se a criança já tiver saído, conforme mostrado no script abaixo. A solução que escolhi foi simplesmente chamar wait -nN vezes, em que N é o número de filhos que tenho e que conheço deterministicamente.

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

Resultado:

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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.