Como esperar em bash para vários subprocessos para terminar e saída de retorno código! = 0 quando qualquer pontas subprocesso com código! = 0?
Pergunta
Como esperar em um script bash para vários subprocessos gerados a partir desse script para acabamento e regresso código de saída! = 0 quando qualquer um dos subprocessos termina com código! = 0?
script simples:
#!/bin/bash
for i in `seq 0 9`; do
doCalculations $i &
done
wait
O script acima irá esperar por todos os 10 subprocessos desovado, mas vai sempre dar status de saída 0 (veja help wait
). Como posso modificar esse script para que ele irá descobrir status de saída de subprocessos desovado e código 1 saída de retorno quando qualquer um dos subprocessos termina com código! = 0?
Existe alguma solução melhor para isso do que a coleta PIDs dos subprocessos, esperar por eles em ordem e soma status de saída?
Solução
wait
também (opcionalmente) converte o PID do processo para esperar, e com US $! você começa a PID do último comando lançado em background.
Modificar o circuito para armazenar o PID de cada sub-processo gerado para uma matriz, e em seguida, repetir novamente à espera 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
Outras dicas
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
Se você tem GNU Parallel instalado, você pode fazer:
# If doCalculations is a function
export -f doCalculations
seq 0 9 | parallel doCalculations {}
GNU Parallel lhe dará o código de saída:
-
0 -. Todos os trabalhos correu sem erros
-
1-253 - Alguns dos trabalhos falhou. O status de saída dá o número de trabalhos com falha
-
254 - Mais de 253 postos de trabalho falhou
.
-
255 -. Outro erro
Assista os vídeos de introdução para saber mais: http://pi.dk/1
Aqui está o que eu vim acima com a medida. Eu gostaria de ver como interromper o comando de sono, se uma criança termina, de modo que um não teria que ajustar WAITALL_DELAY
a sua utilização.
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
Aqui está um exemplo simples usando wait
.
executar alguns processos:
$ sleep 10 &
$ sleep 10 &
$ sleep 20 &
$ sleep 20 &
Em seguida, esperar por eles com o comando wait
:
$ wait < <(jobs -p)
Ou wait
apenas (sem argumentos) para todos.
Isto irá esperar por todas as tarefas em segundo plano estão concluídas.
Se a opção -n
é fornecido, aguarda o próximo trabalho para terminar e retorna seu estado de saída.
Veja:. help wait
e help jobs
para a sintaxe
No entanto, a desvantagem é que este irá retornar apenas no status do último ID, então você precisa para verificar o estado de cada subprocesso e armazená-lo na variável.
Ou fazer a sua função de cálculo para criar algum arquivo em caso de falha (vazio ou com falha log), em seguida, verificar de que arquivo se existe, por exemplo.
$ sleep 20 && true || tee fail &
$ sleep 20 && false || tee fail &
$ wait < <(jobs -p)
$ test -f fail && echo Calculation failed.
Que tal simplesmente:
#!/bin/bash
pids=""
for i in `seq 0 9`; do
doCalculations $i &
pids="$pids $!"
done
wait $pids
...code continued here ...
Update:
Como apontado por vários comentadores, as esperas acima para todos os processos para ser concluída antes de continuar, mas não sair e não conseguem se um deles falhar, ele pode ser feito a ver 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 ...
Para paralelizar isso ...
for i in $(whatever_list) ; do
do_something $i
done
traduzi-lo para isso ...
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, não vai interromper os outros processos, mas que irá resultar em um código de saída diferente de zero a partir da sequência como um todo .
- Exportar funções e variáveis ??pode ou não ser necessário, em qualquer caso particular.
- Você pode definir
--max-procs
baseado em quanto paralelismo você quer (meios0
"tudo de uma vez"). - GNU paralelas ofertas alguns recursos adicionais quando usado no lugar de
xargs
- mas isn 't sempre instalado por padrão. - O loop
for
não é estritamente necessário, neste exemplo, desdeecho $i
é basicamente apenas regenerar a saída do$(whatever_list
). Eu só acho que o uso da palavra-chavefor
torna um pouco mais fácil ver o que está acontecendo. - manipulação de strings Bash pode ser confuso -. Eu descobri que usando aspas simples funciona melhor para acondicionamento de scripts não triviais
- Você facilmente pode interromper toda a operação (usando ^ C ou similar), ao contrário da abordagem mais direta para Bash paralelismo .
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
}'
Eu não acredito que é possível com a funcionalidade embutida de Bash.
Você pode obter uma notificação quando uma criança sai:
#!/bin/sh
set -o monitor # enable script job control
trap 'echo "child died"' CHLD
No entanto não há nenhuma maneira aparente para obter status de saída da criança no manipulador de sinal.
Conseguir que o estado da criança é geralmente o trabalho da família wait
de funções no nível inferior POSIX APIs. Infelizmente o apoio do Bash para isso é limitado - você pode esperar por um processo filho específico (e obter seu status de saída) ou você pode esperar por todas deles, e sempre obter uma 0 resultado.
O que parece impossível de fazer é o equivalente a waitpid(-1)
, que bloqueia até qualquer processos criança retornos.
Eu vejo muitos bons exemplos listados aqui, queria jogar mina no bem.
#! /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
Eu uso algo muito semelhante ao iniciar / servidores de parada / serviços em paralelo e verificar cada estado de saída. Funciona muito bem para mim. Espero que isso ajude alguém!
O código a seguir irá aguardar a conclusão de todos os cálculos e saída de retorno de estado 1 se qualquer um de doCalculations falha.
#!/bin/bash
for i in $(seq 0 9); do
(doCalculations $i >&2 & wait %1; echo $?) &
done | grep -qv 0 && exit 1
Apenas armazenar os resultados para fora da casca, 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
Aqui está a minha versão que funciona para várias pids, registra avisos se a execução leva muito tempo, e pára os subprocessos se a execução demora 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
}
Exemplo, espera para todos os três processos para acabamento, ingresse um aviso se a execução leva loger de 5 segundos, parar todos os processos se a execução demora mais do que 120 segundos. Não programa de saída de 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
Se você tem o bash 4.2 ou posterior disponível o seguinte pode ser útil para você. Ele usa arrays associativos para armazenar os nomes de tarefas e seu "código", bem como os nomes das tarefas e suas pids. Eu também têm construído em um método de limitação de taxa simples, que pode vir a calhar se suas tarefas consomem muita CPU ou I / hora O e você quiser limitar o número de tarefas simultâneas.
Os lançamentos de script todas as tarefas do primeiro ciclo e consome os resultados na segunda.
Este é um exagero bit para casos simples mas permite muito material puro. Por exemplo, pode armazenar mensagens de erro para cada tarefa em outra matriz associativa e imprimi-los depois que tudo se acalmou.
#! /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
Eu apenas fui modificando um script para fundo e parallelise um processo.
Eu fiz algumas experiências (no Solaris com ambos bash e ksh) e descobriu que 'espera' emite o status de saída, se não é zero, ou uma lista de empregos que retornam não zero saída quando nenhum argumento PID é fornecido. Por exemplo.
Bash:
$ 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 é escrito para stderr, então uma solução simples para o exemplo PO poderia 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 isto:
wait 2> >(wc -l)
também irá retornar uma contagem, mas sem o arquivo tmp. Isso também pode ser usado desta forma, por exemplo:
wait 2> >(if [ `wc -l` -gt 0 ] ; then echo "ERROR"; fi)
Mas isso não é muito mais útil do que o arquivo tmp IMO. Eu não poderia encontrar uma maneira útil para evitar o arquivo tmp ao mesmo tempo, evitando a execução do "espera" em um subnível, que não vai funcionar em tudo.
Eu tive um ir neste e combinado todas as melhores partes de outros exemplos aqui. Este script irá executar a função checkpids
quando qualquer sai do processo de fundo, e saída do estado de saída, sem recorrer a votação.
#!/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
#!/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 como o processo que foregrounds -
while fg
vai parar de looping quando todas as saídasfg
com um status de saída diferente de zero
Infelizmente isso não vai lidar com o caso quando um processo nas saídas de fundo com um status de saída diferente de zero. (O loop não será encerrado imediatamente. Ele vai esperar para os processos anteriores para ser concluído.)
Isso funciona, deve ser apenas como um bom se não melhor do que resposta 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
e, claro, eu imortalizaram esse script, em um projeto NPM que lhe permite executar comandos bash em paralelo, útil para testar:
armadilha é seu amigo. Você pode prender em ERR em um monte de sistemas. Você pode prender saída, ou em DEBUG para executar um pedaço de código após cada comando.
Este, além de todos os sinais padrão.
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
no topo faz sua parada roteiro em caso de falha.
expect
voltará 1
se houver subtarefa falhou.
Já há um monte de respostas aqui, mas estou surpreendido ninguém parece ter sugerido usando 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
Isso é algo que eu uso:
#wait for jobs
for job in `jobs -p`; do wait ${job}; done
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í pode-se facilmente extrapolar, e tem um gatilho (tocar um arquivo, enviar um sinal) e alterar os critérios de contagem (arquivos contagem tocado, ou qualquer outro) para responder a esse gatilho. Ou se você quer apenas 'qualquer' rc não zero, apenas matar o bloqueio de save_status.
Eu precisava disso, mas o processo de destino não era uma criança de shell atual, caso em que wait $PID
não funciona. Eu encontrei a seguinte alternativa em vez disso:
while [ -e /proc/$PID ]; do sleep 0.1 ; done
Isso depende da presença de procfs , que pode não estar disponível (Mac não fornecê-la por exemplo). Assim, para a portabilidade, você poderia usar este em vez disso:
while ps -p $PID >/dev/null ; do sleep 0.1 ; done
sinal Trapping CHLD pode não trabalho, porque você pode perder alguns sinais se eles chegaram ao mesmo tempo.
#!/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 !!!
solução para esperar por vários subprocessos e saída quando qualquer um deles sai com código de status diferente de zero é usando 'espera -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
código de status '127' é para o processo de não-existente, o que significa que a criança pode ter saído.
Pode haver um caso em que o processo está completo antes de aguardar o processo. Se desencadear esperar por um processo que já está terminado, ele irá acionar um erro como pid não é um filho deste shell. Para evitar tais 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"
}
Eu acho que a maneira mais simples para executar tarefas em paralelo e verifique se o status está 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 é thread-safe. Se você está preocupado que o código acima estará funcionando ao mesmo tempo como a própria, é melhor usar um nome de arquivo mais original, como falhar. $$. A última linha é para cumprir a exigência: "código de saída de retorno 1 quando qualquer um dos subprocessos termina com código = 0!?" Eu joguei um requisito adicional lá para limpar. Pode ter sido mais clara para escrevê-lo como este:
#!/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 os resultados de vários trabalhos: Eu criar um diretório temporário, história as saídas de todas as sub tarefas em um arquivo separado, e depois despejá-los para revisão. Isto realmente não coincidir com a pergunta - Eu estou jogando-o 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
Estou pensando doCalculations talvez executados; echo "$?" >> / tmp / acc em um subshell , que é enviado para o fundo, então a espera, em seguida, / tmp / acc conteria os status de saída, um por linha. Eu não sei sobre quaisquer consequências dos múltiplos processos de anexação no arquivo acumulador, no entanto.
Aqui está um julgamento desta sugestão:
arquivo: doCalcualtions
#!/bin/sh random -e 20 sleep $? random -e 10
arquivo: tentativa
#!/bin/sh rm /tmp/acc for i in $( seq 0 20 ) do ( ./doCalculations "$i"; echo "$?" >>/tmp/acc ) & done wait cat /tmp/acc | fmt rm /tmp/acc
Saída de correr ./try
5 1 9 6 8 1 2 0 9 6 5 9 6 0 0 4 9 5 5 9 8