Как дождаться завершения в bash нескольких подпроцессов и возврата кода завершения! = 0, когда любой подпроцесс заканчивается кодом! = 0?

563

Как ожидать в bash-скрипте нескольких подпроцессов, порожденных этим скриптом, чтобы завершить работу и вернуть код завершения! = 0, когда любой из подпроцессов заканчивается кодом! = 0?

Простой скрипт:

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

Приведенный выше скрипт будет ожидать всех 10 порожденных подпроцессов, но он всегда будет иметь статус выхода 0 (см. help wait). Как я могу изменить этот скрипт, чтобы он обнаруживал состояния выхода порожденных подпроцессов и возвращал код выхода 1, когда любой из подпроцессов заканчивался кодом! = 0?

Есть ли лучшее решение для этого, чем сбор PID подпроцессов, ожидание их в порядке и суммирование состояний выхода?

tkokoszka
источник
1
Это может быть значительно улучшено wait -n, доступно в современном bash для возврата только после завершения первой / следующей команды.
Чарльз Даффи
если вы хотите проверить с помощью Bash, попробуйте это: github.com/sstephenson/bats
Александр Миллс
2
Активные разработки BATS перешли на github.com/bats-core/bats-core
Potherca
3
У @CharlesDuffy wait -nесть одна небольшая проблема: если не осталось дочерних заданий (так называемое состояние гонки), он возвращает ненулевое состояние завершения (сбой), которое может быть неотличимо от сбойного дочернего процесса.
drevicko
5
@CharlesDuffy - у вас есть прекрасное понимание, и вы делаете огромный сервис для SO, делясь им. Кажется, что около 80% сообщений SO, которые я читаю, делятся замечательными бриллиантами знаний в комментариях, которые должны быть получены из огромного океана опыта. Большое спасибо!
Бретт Холман

Ответы:

521

waitтакже (опционально) принимает PID ожидаемого процесса и с $! Вы получаете PID последней команды, запущенной в фоновом режиме. Измените цикл, чтобы сохранить PID каждого порожденного подпроцесса в массиве, а затем снова выполните цикл ожидания каждого 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
Итак, поскольку вы будете ждать всех процессов, не имеет значения, если, например, вы ожидаете первого, в то время как второе уже завершено (второе все равно будет выбрано на следующей итерации). Это тот же подход, который вы использовали бы в C с wait (2).
Лука Теттаманти
7
Ах, я вижу - другая интерпретация :) Я читаю вопрос как означающий «возврат кода выхода 1 сразу после выхода любого из подпроцессов».
Альнитак
56
PID действительно может быть повторно использован, но вы не можете ждать процесс, который не является дочерним по отношению к текущему процессу (в этом случае ожидание не выполняется).
tkokoszka
12
Вы также можете использовать% n для ссылки на n: th фоновое задание и %% для ссылки на самое последнее.
Конни
30
@Nils_M: Ты прав, извини. Так что это будет что - то вроде: for i in $n_procs; do ./procs[${i}] & ; pids[${i}]=$!; done; for pid in ${pids[*]}; do wait $pid; done;, не так ли?
синацкий день
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
источник
104
jobs -pдает PID подпроцессов, которые находятся в состоянии выполнения. Процесс будет пропущен, если процесс завершится до jobs -pвызова. Таким образом, если какой-либо из подпроцессов завершится раньше jobs -p, состояние выхода этого процесса будет потеряно.
tkokoszka
15
Ничего себе, этот ответ намного лучше, чем самый лучший. : /
e40
4
@ e40 и ответ ниже, вероятно, даже лучше. И еще лучше, вероятно, было бы выполнить каждую команду с помощью '(cmd; echo "$?" >> "$ tmpfile"), использовать это ожидание, а затем прочитать файл на предмет сбоев. Также аннотировать-вывод. ... или просто используйте этот сценарий, когда вам все равно.
HoverHell
Я хотел бы добавить, что этот ответ лучше принятого
shurikk
2
@tkokoszka, чтобы быть точным jobs -p, не дает PID подпроцессов, а вместо этого GPID . Кажется, что логика ожидания работает в любом случае, она всегда ожидает группу, если такая группа существует, и pid, если нет, но это полезно знать ... особенно, если кто-то должен опираться на это и включать что-то вроде отправки сообщений в подпроцесс, в котором случай, когда синтаксис отличается в зависимости от того, есть ли у вас PID или GPID .. т.е. kill -- -$GPIDпротивkill $PID
Тимо
59

Вот простой пример использования wait.

Запустите несколько процессов:

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

Затем дождитесь их waitкомандой:

$ wait < <(jobs -p)

Или просто wait(без аргументов) для всех.

Это будет ожидать завершения всех заданий в фоновом режиме.

Если -nопция указана, ждет завершения следующего задания и возвращает статус завершения.

Смотрите: help waitи help jobsдля синтаксиса.

Однако недостатком является то, что это вернет только состояние последнего идентификатора, поэтому вам нужно проверить состояние каждого подпроцесса и сохранить его в переменной.

Или сделайте свою функцию вычисления, чтобы создать какой-либо файл при сбое (пустой или с ошибкой), а затем проверить этот файл, если он существует, например,

$ sleep 20 && true || tee fail &
$ sleep 20 && false || tee fail &
$ wait < <(jobs -p)
$ test -f fail && echo Calculation failed.
kenorb
источник
2
Для тех , кто новичок в Баш, два вычисления в примере здесь sleep 20 && trueи sleep 20 && false- а именно: заменить те , с функцией (ов). Чтобы понять &&и ||, запустите man bashи введите «/» (поиск), затем «^ * Lists» (регулярное выражение), затем введите: man прокрутите вниз до описания &&и||
drevicko
1
Вы, вероятно, должны проверить, что файл «fail» не существует в начале (или удалить его). В зависимости от приложения, также может быть хорошей идеей добавить '2> & 1' перед тем, ||как перехватить STDERR при сбое.
drevicko
Мне нравится этот, какие-нибудь недостатки? на самом деле, только когда я хочу перечислить все подпроцесс и предпринять некоторые действия, например. отправить сигнал, что я буду пытаться вести учет pids или повторять задания. Ждите финиша, простоwait
xgwang
Это будет пропускать статус выхода задания, которое не удалось до
вызова
50

Если у вас установлен GNU Parallel, вы можете сделать:

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

GNU Parallel даст вам код выхода:

  • 0 - все задания выполнены без ошибок.

  • 1-253 - Не удалось выполнить некоторые задания. Статус выхода дает количество неудачных заданий

  • 254 - более 253 рабочих мест не удалось.

  • 255 - Другая ошибка.

Посмотрите вступительные видео, чтобы узнать больше: http://pi.dk/1

Оле Танге
источник
1
Спасибо! Но ты забыл упомянуть о «путаница» вопрос , который я впоследствии упал в: unix.stackexchange.com/a/35953
nobar
1
Это выглядит как отличный инструмент, но я не думаю, что вышеописанное работает как есть в сценарии Bash, где doCalculationsесть функция, определенная в том же сценарии (хотя OP не прояснил это требование). Когда я пытаюсь, parallelговорит /bin/bash: doCalculations: command not found(он говорит это 10 раз для seq 0 9примера выше). Смотрите здесь для обхода.
Нобар
3
Также представляет интерес: xargsимеет возможность запускать задания параллельно с помощью -Pопции. От сюда : export -f doCalculations ; seq 0 9 |xargs -P 0 -n 1 -I{} bash -c "doCalculations {}". Ограничения xargsперечислены в справочной странице для parallel.
Нобар
И если doCalculationsиспользуются какие-либо другие переменные среды сценария (пользовательские PATHи т. Д.), Их, вероятно, необходимо явно exportотредактировать перед запуском parallel.
Нобар
4
@nobar Путаница из-за того, что некоторые упаковщики портят вещи для своих пользователей. Если вы используете установку, wget -O - pi.dk/3 | shвы не получите путаницы. Если ваш упаковщик все испортил для вас, я призываю вас поднять проблему с вашим упаковщиком. Переменные и функции должны быть экспортированы (экспорт -f) для GNU Parallel, чтобы увидеть их (см . man parallel: gnu.org/software/parallel/… )
Ole Tange
46

Как насчет просто:

#!/bin/bash

pids=""

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

wait $pids

...code continued here ...

Обновить:

Как указывалось несколькими комментаторами, приведенное выше ожидает завершения всех процессов перед продолжением, но не завершается и не завершается ошибкой в ​​случае сбоя одного из них. Это можно сделать с помощью следующей модификации, предложенной @Bryan, @SamBrightman и другими. :

#!/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
источник
1
Согласно man-страницам wait, wait с несколькими PID возвращает только значение, возвращенное последним ожидающим процессом. Поэтому вам нужен дополнительный цикл и ждать каждого PID отдельно, как это предлагается в принятом ответе (в комментариях).
Влад Фролов
1
Поскольку на этой странице больше нигде не указано, я добавлю, что цикл будетfor pid in $pids; do wait $pid; done
Брайан
1
@bisounours_tronconneuse да, вы делаете. Смотрите help wait- с несколькими идентификаторами waitвозвращает код выхода только последнего, как было сказано выше @ vlad-frolov.
Сэм Брайтман
1
Брайан, @SamBrightman Хорошо. Я изменил это с вашими рекомендациями.
patapouf_ai
4
У меня была очевидная проблема с этим решением: что если данный процесс завершится до того, как waitбудет вызван соответствующий процесс ? Оказывается, это не проблема: если вы находитесь waitв процессе, который уже завершен, waitнемедленно выйдите из него со статусом уже завершившегося процесса. (Спасибо, bashавторы!)
Даниэль Гриском,
39

Вот что я придумала до сих пор. Я хотел бы увидеть, как прервать команду сна, если ребенок завершает работу, чтобы не пришлось настраиваться WAITALL_DELAYна его использование.

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
Марк Эдгар
источник
Можно было бы пропустить этот WAITALL_DELAY или установить его очень низким, так как внутри цикла не запускаются никакие процессы, я не думаю, что это слишком дорого.
Мариан
21

Чтобы распараллелить это ...

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

Переведите это на это ...

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 {}
      }' 
   )
  • Если ошибка возникает в одном процессе, она не прерывает другие процессы, но приводит к ненулевому коду выхода из последовательности в целом .
  • Экспорт функций и переменных может или не может быть необходимым, в любом конкретном случае.
  • Вы можете установить --max-procsна основе того, сколько параллелизма вы хотите ( 0означает «все сразу»).
  • GNU Parallel предлагает некоторые дополнительные функции при использовании вместо xargs- но он не всегда устанавливается по умолчанию.
  • forЦикл не является строго необходимым в этом примере , так как echo $iв основном только регенерирующий выход $(whatever_list). Я просто думаю, что использование forключевого слова немного облегчает понимание того, что происходит.
  • Обработка строк в Bash может сбивать с толку - я обнаружил, что использование одинарных кавычек лучше всего подходит для упаковки нетривиальных сценариев.
  • Вы можете легко прервать всю операцию (используя ^ C или подобное), в отличие от более прямого подхода к параллелизму Bash .

Вот упрощенный рабочий пример ...

for i in {0..5} ; do echo $i ; done |xargs -I{} --max-procs 2 bash -c '
   {
   echo sleep {}
   sleep 2s
   }'
nobar
источник
7

Я не верю, что это возможно с помощью встроенной функциональности Bash.

Вы можете получить уведомление, когда ребенок выходит:

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

Однако нет очевидного способа получить статус выхода ребенка в обработчике сигнала.

Получение этого дочернего статуса обычно является задачей waitсемейства функций в API POSIX нижнего уровня. К сожалению, поддержка Bash для этого ограничена - вы можете подождать одного конкретного дочернего процесса (и получить его статус завершения) или вы можете подождать всех их, и всегда получите 0 результата.

То, что кажется невозможным сделать, - это эквивалент waitpid(-1), который блокирует, пока какой-либо дочерний процесс не вернется.

Альнитак
источник
7

Я вижу много хороших примеров, перечисленных здесь, хотел бы добавить и мой.

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

Я использую что-то очень похожее для параллельного запуска / остановки серверов / сервисов и проверки каждого состояния выхода. Прекрасно работает для меня. Надеюсь, это поможет кому-то!

Джейсон Слобоцки
источник
Когда я останавливаю это с помощью Ctrl + C, я все еще вижу процессы, запущенные в фоновом режиме.
Карстен
2
@karsten - это другая проблема. Предполагая, что вы используете bash, вы можете перехватить условие выхода (включая Ctrl + C) и уничтожить текущий и все дочерние процессы с помощьюtrap "kill 0" EXIT
Phil
@ Фил правильно. Поскольку это фоновые процессы, уничтожение родительского процесса просто оставляет все дочерние процессы запущенными. Мой пример не улавливает никаких сигналов, которые могут быть добавлены в случае необходимости, как сказал Фил.
Джейсон
6

Это то, что я использую:

#wait for jobs
for job in `jobs -p`; do wait ${job}; done
jplozier
источник
5

Следующий код будет ожидать завершения всех вычислений и вернет состояние выхода 1, если произойдет сбой любого из doCalculations .

#!/bin/bash
for i in $(seq 0 9); do
   (doCalculations $i >&2 & wait %1; echo $?) &
done | grep -qv 0 && exit 1
эээ
источник
5

Просто сохраните результаты из оболочки, например, в файле.

#!/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
источник
5

Вот моя версия, которая работает для нескольких pids, регистрирует предупреждения, если выполнение занимает слишком много времени, и останавливает подпроцессы, если выполнение занимает больше заданного значения.

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
}

Например, дождитесь завершения всех трех процессов, запишите предупреждение, если выполнение занимает менее 5 секунд, остановите все процессы, если выполнение займет более 120 секунд. Не выходите из программы при сбоях.

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

Если у вас есть bash 4.2 или более поздняя версия, вам может быть полезно следующее. Он использует ассоциативные массивы для хранения имен задач и их «кода», а также имен задач и их pids. Я также встроил простой метод ограничения скорости, который может пригодиться, если ваши задачи потребляют много ресурсов ЦП или ввода-вывода и вы хотите ограничить количество одновременных задач.

Скрипт запускает все задачи в первом цикле и использует результаты во втором.

Это немного излишне для простых случаев, но допускает довольно аккуратные вещи. Например, можно хранить сообщения об ошибках для каждой задачи в другом ассоциативном массиве и распечатывать их после того, как все установится.

#! /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
источник
4

Я только что изменил сценарий, чтобы фон и распараллелить процесс.

Я провел некоторые эксперименты (в Solaris с bash и ksh) и обнаружил, что «wait» выводит состояние выхода, если оно не равно нулю, или список заданий, которые возвращают ненулевой выход, если не указан аргумент PID. Например

Bash:

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

КШ:

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

Этот вывод записывается в stderr, поэтому простое решение для примера OP может быть:

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

Пока это:

wait 2> >(wc -l)

также вернет счетчик, но без файла tmp. Это также может быть использовано следующим образом, например:

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

Но это не намного полезнее, чем IMO-файл tmp. Я не смог найти полезного способа избежать файла tmp, а также избежать запуска «wait» в подоболочке, которая вообще не будет работать.

вздор
источник
3

Я попробовал это и объединил все лучшие части из других примеров здесь. Этот скрипт будет выполнять checkpidsфункцию при выходе из любого фонового процесса и выводить состояние выхода, не прибегая к опросу.

#!/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
источник
3
#!/bin/bash
set -m
for i in `seq 0 9`; do
  doCalculations $i &
done
while fg; do true; done
  • set -m позволяет использовать fg & bg в скрипте
  • fgв дополнение к тому, что последний процесс находится на переднем плане, имеет тот же статус выхода, что и процесс, который он на переднем плане
  • while fgпрекратит зацикливание, когда любые fgвыходы с ненулевым статусом выхода

к сожалению, это не будет обрабатывать случай, когда процесс в фоновом режиме завершает работу с ненулевым состоянием выхода. (Цикл не прекратится немедленно. Он будет ждать завершения предыдущих процессов.)

Jayen
источник
3

Здесь уже есть много ответов, но я удивлен, что никто, похоже, не предложил использовать массивы ... Итак, вот что я сделал - это может быть полезно для некоторых в будущем.

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
источник
3

Это работает, должно быть так же хорошо, если не лучше, чем ответ @ 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

и, конечно, я увековечил этот сценарий в проекте NPM, который позволяет вам параллельно запускать команды bash, полезные для тестирования:

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

Александр Миллс
источник
trap $? $$Кажется, каждый раз для меня устанавливается код выхода 0 и PID для текущей запущенной оболочки bash
inntknght
Вы абсолютно уверены в этом? Не уверен, имеет ли это смысл.
Александр Миллс
2

ловушка твой друг. Вы можете поймать ERR во многих системах. Вы можете перехватить EXIT или DEBUG, чтобы выполнить фрагмент кода после каждой команды.

Это в дополнение ко всем стандартным сигналам.

Пол Ходжес
источник
1
Пожалуйста, не могли бы вы уточнить свой ответ с некоторыми примерами.
Ϲοδε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

set -eВ верхней делает вашу остановку сценария на провал.

expectвернется, 1если какая-либо работа не удалась.

Yajo
источник
2

Именно для этого я написал bashфункцию под названием :for.

Примечание : :forне только сохраняет и возвращает код завершения сбойной функции, но также завершает все параллельно работающие экземпляры. Который может не понадобиться в этом случае.

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

Применение

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

Ссылки

Ente
источник
1

Я использовал это недавно (спасибо 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

Оттуда можно легко экстраполировать и иметь триггер (коснуться файла, отправить сигнал) и изменить критерии подсчета (количество файлов, к которым обращались или что-то еще), чтобы ответить на этот триггер. Или, если вы просто хотите «любой» ненулевой rc, просто убейте блокировку из save_status.

Lloeki
источник
1

Мне это нужно, но целевой процесс не был дочерним по отношению к текущей оболочке, в этом случае wait $PIDне работает. Вместо этого я нашел следующую альтернативу:

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

Это зависит от наличия procfs , который может быть недоступен (например, Mac не предоставляет его). Так что для переносимости вы можете использовать это вместо:

while ps -p $PID >/dev/null ; do sleep 0.1 ; done
troelskn
источник
1

Сигнал перехвата CHLD может не работать, потому что вы можете потерять некоторые сигналы, если они поступили одновременно.

#!/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
источник
1

Решение для ожидания нескольких подпроцессов и выхода, когда любой из них завершается с ненулевым кодом состояния, заключается в использовании 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

Код состояния «127» предназначен для несуществующего процесса, что означает, что ребенок мог выйти.

vishnuitta
источник
1

Дождитесь всех заданий и верните код завершения последнего невыполненного задания. В отличие от решений выше, это не требует сохранения pid. Просто подожди и подожди.

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
}
Эрик Аронесты
источник
Это сработает и надежно выдаст первый код ошибки из ваших выполненных команд, если только это не «команда не найдена» (код 127).
Древицко,
0

Может быть случай, когда процесс завершен, прежде чем ждать его. Если мы инициируем ожидание процесса, который уже завершен, он вызовет ошибку, как pid не является дочерним элементом этой оболочки. Чтобы избежать таких случаев, можно использовать следующую функцию, чтобы определить, завершен ли процесс или нет:

isProcessComplete(){
PID=$1
while [ -e /proc/$PID ]
do
    echo "Process: $PID is still running"
    sleep 5
done
echo "Process $PID has finished"
}
Анжу Прасаннан
источник
0

Я думаю, что самый простой способ параллельно выполнять задания и проверять их состояние - использовать временные файлы. Уже есть пара похожих ответов (например, Nietzche-jou и mug896).

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

Приведенный выше код не является потокобезопасным. Если вы обеспокоены тем, что приведенный выше код будет работать одновременно с самим собой, лучше использовать более уникальное имя файла, например fail. $$. Последняя строка должна выполнить требование: «вернуть код выхода 1, когда любой из подпроцессов заканчивается кодом! = 0?» Я бросил дополнительное требование, чтобы убрать. Возможно, было бы яснее написать это так:

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

Вот аналогичный фрагмент для сбора результатов из нескольких заданий: я создаю временный каталог, записываю выходные данные всех подзадач в отдельный файл и затем выгружаю их для просмотра. Это не совсем соответствует вопросу - я добавляю это в качестве бонуса:

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

Я чуть не попал в ловушку использования jobs -pPID для сбора PID, что не работает, если ребенок уже вышел, как показано в приведенном ниже сценарии. Решением, которое я выбрал, было просто вызвать wait -nN раз, где N - это число моих детей, которое я знаю детерминистически.

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

Вывод:

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
Даниэль С. Собрал
источник