Jak czekać w bash na kilka podprocesów, aby zakończyć i zwrócić kod zakończenia! = 0, gdy dowolny podproces kończy się kodem! = 0?


561

Jak czekać w skrypcie bash na kilka podprocesów odrodzonych z tego skryptu, aby zakończyć i zwrócić kod zakończenia! = 0, gdy którykolwiek z podprocesów kończy się kodem! = 0?

Prosty skrypt:

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

Powyższy skrypt będzie czekał na wszystkie 10 odrodzonych podprocesów, ale zawsze da status wyjścia 0 (patrz help wait). Jak mogę zmodyfikować ten skrypt, aby wykrył status wyjścia spawnowanych podprocesów i zwrócił kod wyjścia 1, gdy którykolwiek z podprocesów zakończy się kodem! = 0?

Czy istnieje na to lepsze rozwiązanie niż zbieranie PID podprocesów, czekanie na nie w kolejności i sumowanie stanów wyjścia?


1
Można to znacznie poprawić dotykając wait -n, dostępne w nowoczesnym bashu, aby powrócić dopiero po zakończeniu pierwszego / następnego polecenia.
Charles Duffy,

jeśli chcesz przetestować za pomocą Bash, spróbuj tego: github.com/sstephenson/bats
Alexander Mills

2
Aktywny rozwój BATS został przeniesiony na github.com/bats-core/bats-core
Potherca

3
@CharlesDuffy wait -nma jeden mały problem: jeśli nie ma już zadań potomnych (inaczej warunek wyścigu), zwraca niezerowy status wyjścia (niepowodzenie), który może być nie do odróżnienia od nieudanego procesu potomnego.
drevicko

5
@CharlesDuffy - Masz wspaniały wgląd i wykonujesz ogromną usługę dla SO, dzieląc się nią. Wygląda na to, że około 80% postów SO, które przeczytałem, dzieli się wspaniałymi diamentami wiedzy w komentarzach, które muszą pochodzić z ogromnego oceanu doświadczeń. Wielkie dzięki!
Brett Holman

Odpowiedzi:


519

waittakże (opcjonalnie) pobiera PID procesu, na który czeka, i za pomocą $! otrzymasz PID ostatniego polecenia uruchomionego w tle. Zmodyfikuj pętlę, aby przechowywać PID każdego odrodzonego podprocesu w tablicy, a następnie zapętl ponownie, czekając na każdy 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
Weel, ponieważ będziesz czekał na wszystkie procesy, nie ma znaczenia, czy np. Czekasz na pierwszy, gdy drugi już się zakończył (drugi i tak zostanie wybrany przy następnej iteracji). Jest to to samo podejście, którego użyłbyś w C z funkcją wait (2).
Luca Tettamanti,

7
Ach, rozumiem - inna interpretacja :) Przeczytałem pytanie jako „zwróć kod wyjścia 1 natychmiast po wyjściu dowolnego z podprocesów”.
Alnitak,

56
PID może być rzeczywiście ponownie użyty, ale nie można czekać na proces, który nie jest potomkiem bieżącego procesu (w takim przypadku czekanie kończy się niepowodzeniem).
tkokoszka

12
Możesz także użyć% n, aby odnieść się do n-tego zadania w tle, a %%, aby odnieść się do najnowszego.
conny

30
@Nils_M: Masz rację, przepraszam. To byłoby coś w stylu:, for i in $n_procs; do ./procs[${i}] & ; pids[${i}]=$!; done; for pid in ${pids[*]}; do wait $pid; done;prawda?
Synack

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 -ppodaje PID podprocesów, które są w stanie wykonania. Pominie proces, jeśli proces zakończy się przed jobs -pwywołaniem. Więc jeśli którykolwiek z podprocesów zakończy się wcześniej jobs -p, status zakończenia tego procesu zostanie utracony.
tkokoszka

15
Wow, ta odpowiedź jest znacznie lepsza niż najwyżej oceniona. : /
e40

4
@ e40 i odpowiedź poniżej jest prawdopodobnie jeszcze lepsza. A jeszcze lepiej byłoby prawdopodobnie uruchomić każdą komendę za pomocą '(cmd; echo "$?" >> "$ tmpfile"), użyj tego czekania, a następnie odczytaj plik w przypadku niepowodzenia. Wyjście adnotacji. … Lub po prostu użyj tego skryptu, jeśli nie przejmujesz się tym zbytnio.
HoverHell

Chciałbym dodać, że ta odpowiedź jest lepsza niż zaakceptowana
shurikk

2
@ tkokoszka, aby być dokładnym, jobs -pnie podaje PID podprocesów, ale zamiast tego GPID . Wygląda na to, że logika oczekiwania i tak działa, zawsze czeka na grupę, jeśli taka grupa istnieje, i pid, jeśli nie, ale dobrze jest być świadomym ... szczególnie, jeśli ktoś ma się na tym opierać i włączyć coś w rodzaju wysyłania wiadomości do podprocesu, w którym przypadek składnia jest różna w zależności od tego, czy masz PID, czy GPID .. tzn. kill -- -$GPIDvskill $PID
Timo

58

Oto prosty przykład użycia wait.

Uruchom niektóre procesy:

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

Następnie poczekaj na nich z waitpoleceniem:

$ wait < <(jobs -p)

Lub po prostu wait(bez argumentów) dla wszystkich.

Będzie to czekać na zakończenie wszystkich zadań w tle.

Jeśli -nopcja jest podana, czeka na zakończenie następnego zadania i zwraca status wyjścia.

Zobacz: help waiti help jobsskładnia.

Minusem jest jednak to, że zwróci to tylko status ostatniego identyfikatora, dlatego należy sprawdzić status każdego podprocesu i zapisać go w zmiennej.

Lub włącz funkcję obliczeń, aby utworzyć jakiś plik w przypadku awarii (pusty lub z dziennikiem błędów), a następnie sprawdź ten plik, jeśli istnieje, np

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

1
Dla tych, którzy dopiero zaczynają bash, dwa obliczenia w tym przykładzie to sleep 20 && truei sleep 20 && false- tj .: zamień je na swoje funkcje. Aby zrozumieć &&i ||uruchom man bashi wpisz „/” (szukaj), a następnie „^ * Listy” (regex), a następnie wprowadź: człowiek przewinie w dół do opisu &&i||
drevicko

1
powinieneś prawdopodobnie sprawdzić, czy plik „fail” nie istnieje na początku (lub go usunąć). W zależności od aplikacji dobrym pomysłem może być również dodanie „2> i 1” przed ||niepowodzeniem, aby złapać STDERR.
drevicko

podoba mi się ten, jakieś wady? właściwie tylko wtedy, gdy chcę wyświetlić listę wszystkich podprocesów i podjąć pewne działania, np. wyślij sygnał, że postaram się księgować stawki lub iterować zadania. Poczekaj na zakończenie, po prostuwait
xgwang

Spowoduje to pominięcie statusu wyjścia zadania, które nie powiodło się przed wywołaniem zadania -p
Erik Aronesty

50

Jeśli masz zainstalowany GNU Parallel, możesz:

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

GNU Parallel da ci kod wyjścia:

  • 0 - Wszystkie zadania uruchomiono bez błędów.

  • 1-253 - Niektóre zadania nie powiodły się. Status wyjścia podaje liczbę nieudanych zadań

  • 254 - Ponad 253 zadań nie powiodło się.

  • 255 - Inny błąd.

Obejrzyj filmy wprowadzające, aby dowiedzieć się więcej: http://pi.dk/1


1
Dzięki! Ale zapomniałeś wspomnieć o problemie „zamieszania”, do którego później wpadłem: unix.stackexchange.com/a/35953
nobar

1
To wygląda na świetne narzędzie, ale nie sądzę, że powyższe działa tak, jak jest w skrypcie Bash, gdzie doCalculationsjest funkcja zdefiniowana w tym samym skrypcie (chociaż OP nie było jasne na temat tego wymagania). Kiedy próbuję, parallelmówi /bin/bash: doCalculations: command not found(mówi to 10 razy w seq 0 9powyższym przykładzie). Zobacz tutaj obejście.
nobar

3
Interesujące: xargsma pewne możliwości równoległego uruchamiania zleceń za pomocą -Popcji. Od tutaj : export -f doCalculations ; seq 0 9 |xargs -P 0 -n 1 -I{} bash -c "doCalculations {}". Ograniczenia xargssą wyliczone na stronie podręcznika dla parallel.
nobar

A jeśli doCalculationsopiera się na jakichkolwiek innych wewnętrznych zmiennych skryptowych (niestandardowych PATHitp.), Prawdopodobnie należy je jawnie exportedytować przed uruchomieniem parallel.
nobar

4
@nobar Zamieszanie jest spowodowane tym, że niektórzy pakujący psują rzeczy swoim użytkownikom. Jeśli zainstalujesz za pomocą wget -O - pi.dk/3 | sh, nie dostaniesz zamieszania. Jeśli twój program pakujący zawiódł dla ciebie sprawy, zachęcam do poruszenia problemu z programem pakującym. Zmienne i funkcje powinny zostać wyeksportowane (export -f) dla GNU Parallel, aby je zobaczyć (patrz man parallel: gnu.org/software/parallel/... )
Ole Tange

46

A może po prostu:

#!/bin/bash

pids=""

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

wait $pids

...code continued here ...

Aktualizacja:

Jak wskazało wielu komentujących, powyższe czeka na zakończenie wszystkich procesów przed kontynuowaniem, ale nie kończy działania i kończy się niepowodzeniem, jeśli jeden z nich zawiedzie, można to zrobić za pomocą następującej modyfikacji sugerowanej przez @Bryan, @SamBrightman i innych :

#!/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
Zgodnie ze stronami podręcznika wait, czekanie z wieloma PID zwraca tylko wartość zwracaną przez ostatni oczekiwany proces. Potrzebujesz więc dodatkowej pętli i poczekaj na każdy PID osobno, jak sugerowano w zaakceptowanej odpowiedzi (w komentarzach).
Vlad Frolov,

1
Ponieważ wydaje się, że nie podano go nigdzie indziej na tej stronie, dodam, że pętla byłabyfor pid in $pids; do wait $pid; done
Bryan

1
@bisounours_tronconneuse tak, robisz. Zobacz help wait- z wieloma identyfikatorami waitzwraca kod wyjścia tylko ostatniego, jak powiedział @ vlad-frolov.
Sam Brightman,

1
Bryan, @SamBrightman Ok. Zmodyfikowałem to według twoich rekomendacji.
patapouf_ai

4
Miałem oczywiste obawy związane z tym rozwiązaniem: co jeśli dany proces zakończy się przed waitwywołaniem odpowiedniego? Okazuje się, że nie jest to problem: jeśli waitw procesie, który już został zakończony, waitnatychmiast wyjdzie ze statusem już zakończonego procesu. (Dziękuję, bashautorzy!)
Daniel Griscom

39

Oto, co do tej pory wymyśliłem. Chciałbym zobaczyć, jak przerwać komendę uśpienia, jeśli dziecko zakończy działanie, aby nie trzeba było dostosowywać się WAITALL_DELAYdo jego użycia.

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

Można by pominąć ten WAITALL_DELAY lub ustawić go bardzo nisko, ponieważ żadne procesy nie są uruchamiane w pętli, nie sądzę, że jest to zbyt drogie.
Marian

21

Aby zrównoważyć to ...

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

Przetłumacz to na to ...

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 {}
      }' 
   )
  • Jeśli błąd wystąpi w jednym procesie, nie zakłóci on innych procesów, ale spowoduje niezerowy kod wyjścia z sekwencji jako całości .
  • Eksportowanie funkcji i zmiennych może, ale nie musi być konieczne, w każdym konkretnym przypadku.
  • Możesz ustawić --max-procsna podstawie tego, ile chcesz równoległości ( 0oznacza „wszystkie naraz”).
  • GNU Parallel oferuje kilka dodatkowych funkcji, gdy jest używany zamiast xargs- ale nie zawsze jest instalowany domyślnie.
  • W fortym przykładzie pętla nie jest absolutnie niezbędna, ponieważ echo $ipo prostu regeneruje wyjście $(whatever_list). Po prostu myślę, że użycie forsłowa kluczowego sprawia, że ​​trochę łatwiej jest zobaczyć, co się dzieje.
  • Obsługa ciągów bash może być myląca - odkryłem, że używanie pojedynczych cudzysłowów najlepiej sprawdza się przy pakowaniu nietrywialnych skryptów.
  • Możesz łatwo przerwać całą operację (używając ^ C lub podobnego), w przeciwieństwie do bardziej bezpośredniego podejścia do równoległości Basha .

Oto uproszczony przykład działania ...

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


7

Nie sądzę, aby było to możliwe dzięki wbudowanej funkcjonalności Bash.

Państwo może dostać powiadomienie, gdy dziecko opuszcza:

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

Jednak nie ma widocznego sposobu, aby uzyskać status wyjścia dziecka w module obsługi sygnału.

Uzyskanie tego statusu potomnego jest zwykle zadaniem waitrodziny funkcji w niższych poziomach interfejsów API POSIX. Niestety wsparcie Bash na to jest ograniczone - możesz poczekać na jeden konkretny proces potomny (i uzyskać jego status wyjścia) lub możesz poczekać na wszystkie z nich i zawsze uzyskać wynik 0.

To, co wydaje się niemożliwe, to odpowiednik waitpid(-1), który blokuje, dopóki nie powróci żaden proces potomny.


7

Widzę tu wiele dobrych przykładów, które też chciałem wrzucić.

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

Używam czegoś bardzo podobnego do uruchamiania / zatrzymywania serwerów / usług równolegle i sprawdzam status każdego wyjścia. Działa świetnie dla mnie. Mam nadzieję, że to komuś pomoże!


Kiedy zatrzymuję to za pomocą Ctrl + CI, nadal widzę procesy działające w tle.
karsten

2
@karsten - to inny problem. Zakładając, że używasz bash, możesz uwięzić warunek wyjścia (w tym Ctrl + C) i zabić bieżące oraz wszystkie procesy potomne za pomocątrap "kill 0" EXIT
Phil

@Pil jest poprawny. Ponieważ są to procesy działające w tle, zabicie procesu nadrzędnego po prostu pozostawia uruchomione procesy potomne. Mój przykład nie przechwytuje żadnych sygnałów, które można dodać w razie potrzeby, jak stwierdził Phil.
Jason Słobotski

6

Używam tego:

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

5

Poniższy kod będzie czekał na zakończenie wszystkich obliczeń i zwróci status wyjścia 1, jeśli którekolwiek z doCalculations zakończy się niepowodzeniem.

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

5

Po prostu przechowuj wyniki poza powłoką, np. W pliku.

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

Oto moja wersja, która działa na wiele pid, rejestruje ostrzeżenia, jeśli wykonanie trwa zbyt długo, i zatrzymuje podprocesy, jeśli wykonanie trwa dłużej niż podana wartość.

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
}

Przykład: poczekaj, aż wszystkie trzy procesy zakończą się, zaloguj ostrzeżenie, jeśli wykonanie zajmie więcej czasu niż 5 sekund, zatrzymaj wszystkie procesy, jeśli wykonanie zajmie więcej niż 120 sekund. Nie zamykaj programu w przypadku awarii.

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

Jeśli masz wersję bash 4.2 lub nowszą, poniższe mogą być przydatne. Używa tablic asocjacyjnych do przechowywania nazw zadań i ich „kodu”, a także nazw zadań i ich pid. Wbudowałem również prostą metodę ograniczania szybkości, która może się przydać, jeśli twoje zadania zajmują dużo czasu procesora lub wejścia / wyjścia i chcesz ograniczyć liczbę jednoczesnych zadań.

Skrypt uruchamia wszystkie zadania w pierwszej pętli i zużywa wyniki w drugiej.

Jest to trochę przesada w przypadku prostych przypadków, ale pozwala na całkiem porządne rzeczy. Na przykład można przechowywać komunikaty o błędach dla każdego zadania w innej tablicy asocjacyjnej i drukować je po ustąpieniu wszystkiego.

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

Właśnie modyfikowałem skrypt w tle i równolegle proces.

Przeprowadziłem kilka eksperymentów (w systemie Solaris zarówno z bash, jak i ksh) i odkryłem, że „czekanie” wyświetla status wyjścia, jeśli nie jest zero, lub listę zadań, które zwracają niezerowe wyjście, gdy nie podano argumentu PID. Na przykład

Grzmotnąć:

$ 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

Ten wynik jest zapisywany do stderr, więc prostym rozwiązaniem dla przykładu OP może być:

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

Kiedy to:

wait 2> >(wc -l)

zwróci również liczbę, ale bez pliku tmp. Można to również wykorzystać w ten sposób, na przykład:

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

Ale to nie jest o wiele bardziej przydatne niż plik IMO pliku tmp. Nie mogłem znaleźć użytecznego sposobu na uniknięcie pliku tmp, jednocześnie unikając uruchamiania „czekania” w podpowłoce, co w ogóle nie zadziała.


3

Próbowałem tego i połączyłem wszystkie najlepsze części z innych przykładów tutaj. Ten skrypt wykona checkpidsfunkcję po zakończeniu dowolnego procesu w tle i wyśle status wyjścia bez uciekania się do odpytywania.

#!/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 pozwala używać fg i bg w skrypcie
  • fg, oprócz umieszczania ostatniego procesu na pierwszym planie, ma ten sam status wyjścia, co proces, który pierwszy planuje
  • while fgprzestanie zapętlać, gdy którykolwiek fgz wyjść ma niezerowy status wyjścia

niestety nie będzie to obsługiwać przypadku, gdy proces w tle kończy się z niezerowym statusem wyjścia. (pętla nie zakończy się natychmiast. Poczeka na zakończenie poprzednich procesów).


3

Jest tu już wiele odpowiedzi, ale jestem zaskoczony, że nikt nie sugerował się używaniem tablic ... Więc oto co zrobiłem - może to być przydatne dla niektórych w przyszłości.

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

To działa, powinno być tak samo dobre, jeśli nie lepsze niż odpowiedź @ 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

i oczywiście unieśmiertelniłem ten skrypt w projekcie NPM, który pozwala na równoległe uruchamianie poleceń bash, przydatnych do testowania:

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


trap $? $$zdaje się ustawiać kod wyjścia na 0 i PID na bieżącą działającą powłokę bash, za każdym razem dla mnie
inetknght

jesteś tego absolutnie pewien? Nie jestem pewien, czy to ma sens.
Alexander Mills,

2

pułapka jest twoim przyjacielem. Możesz pułapkować na ERR w wielu systemach. Możesz zatrzymać EXIT lub DEBUG, aby wykonać kawałek kodu po każdym poleceniu.

Jest to dodatek do wszystkich standardowych sygnałów.


1
Czy możesz udzielić odpowiedzi na kilka przykładów.
Ϲοδε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

U set -egóry powoduje zatrzymanie skryptu w przypadku niepowodzenia.

expectpowróci, 1jeśli jakiekolwiek subjob zakończy się niepowodzeniem.


2

Właśnie w tym celu napisałem bashfunkcję o nazwie :for.

Uwaga : :fornie tylko zachowuje i zwraca kod wyjścia funkcji powodującej błąd, ale także kończy wszystkie równoległe działające wystąpienia. Co może nie być potrzebne w tym przypadku.

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

stosowanie

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

Bibliografia


1

Użyłem tego niedawno (dzięki 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

Stamtąd można łatwo ekstrapolować i mieć wyzwalacz (dotknąć pliku, wysłać sygnał) i zmienić kryteria liczenia (zliczyć dotknięte pliki lub cokolwiek innego), aby odpowiedzieć na to wyzwalanie. Lub jeśli chcesz 'dowolne' niezerowe rc, po prostu zabij blokadę z save_status.


1

Potrzebowałem tego, ale proces docelowy nie był potomkiem bieżącej powłoki, w którym to przypadku wait $PIDnie działa. Zamiast tego znalazłem następującą alternatywę:

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

Zależy to od obecności procfs , które mogą nie być dostępne (na przykład Mac nie zapewnia tego). Aby zapewnić przenośność, możesz użyć tego:

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

1

Wychwytywanie sygnału CHLD może nie działać, ponieważ niektóre sygnały można utracić, jeśli pojawią się jednocześnie.

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

rozwiązaniem jest czekanie na kilka podprocesów i wyjście, gdy którekolwiek z nich zakończy się z niezerowym kodem stanu, za pomocą „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

kod stanu „127” dotyczy nieistniejącego procesu, co oznacza, że ​​dziecko mogło wyjść.


1

Poczekaj na wszystkie zadania i zwróć kod zakończenia ostatniego zadania, które się nie powiodło. W przeciwieństwie do powyższych rozwiązań nie wymaga to oszczędzania pid. Po prostu odejdź i czekaj.

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
}

Działa to i niezawodnie podaje pierwszy kod błędu z wykonanych poleceń, chyba że jest to „polecenie nie znaleziono” (kod 127).
drevicko

0

Może zdarzyć się, że proces zostanie zakończony przed oczekiwaniem na proces. Jeśli uruchomimy oczekiwanie na proces, który jest już zakończony, spowoduje to błąd, taki jak pid nie jest potomkiem tej powłoki. Aby uniknąć takich przypadków, można użyć następującej funkcji, aby sprawdzić, czy proces jest zakończony, czy nie:

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

0

Myślę, że najprostszym sposobem równoległego uruchamiania zadań i sprawdzania statusu są pliki tymczasowe. Istnieje już kilka podobnych odpowiedzi (np. Nietzche-jou i mug896).

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

Powyższy kod nie jest bezpieczny dla wątków. Jeśli obawiasz się, że powyższy kod będzie działał w tym samym czasie co on sam, lepiej użyć bardziej unikalnej nazwy pliku, np. Fail. $$. Ostatni wiersz ma spełniać warunek: „zwróć kod wyjścia 1, gdy którekolwiek z podprocesów zakończy się kodem! = 0?” Wrzuciłem tam dodatkowy wymóg, aby posprzątać. Być może łatwiej byłoby napisać to w następujący sposób:

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

Oto podobny fragment do zbierania wyników z wielu zadań: tworzę katalog tymczasowy, tworzę wyniki wszystkich zadań podrzędnych w osobnym pliku, a następnie zrzucam je do przeglądu. To naprawdę nie pasuje do pytania - wrzucam to jako 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

0

Prawie wpadłem w pułapkę używania jobs -pdo zbierania identyfikatorów PID, co nie działa, jeśli dziecko już wyszło, jak pokazano w skrypcie poniżej. Rozwiązaniem, które wybrałem, było po prostu wywołanie wait -nN razy, gdzie N jest liczbą dzieci, które mam, które znam zdeterminowane.

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

Wynik:

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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.