Як чекати в bash, щоб кілька підпроцесів закінчити і повернути вихідний код! = 0, коли будь-який підпроцес закінчується кодом! = 0?


562

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

Простий сценарій:

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

Вищеописаний сценарій буде чекати всіх 10 породжених підпроцесів, але він завжди надасть статус виходу 0 (див. help wait) Як я можу змінити цей скрипт, щоб він виявив статуси виходу породжених підпроцесів та повертає вихідний код 1, коли будь-який із підпроцесів закінчується кодом! = 0?

Чи є для цього краще рішення, ніж збирати PID-адреси підпроцесів, чекати їх у порядку та підсумовувати статуси виходу?


1
Це може бути значно покращено, щоб доторкнутися wait -n, доступне в сучасному bash, щоб повернутися лише тоді, коли виконується перша / наступна команда.
Чарльз Даффі

якщо ви хочете протестувати за допомогою Bash, спробуйте це: github.com/sstephenson/bats
Олександр Міллс

2
Активний розвиток BATS
Potherca

3
@CharlesDuffy wait -nмає одну невелику проблему: якщо не залишилося жодної дочірньої роботи (він же стан гонки), він повертає ненульовий статус виходу (невдача), який може бути невідрізним від невдалого дочірнього процесу.
drevicko

5
@CharlesDuffy - Ви чудово розумієте, і ви робите величезний сервіс SO, поділяючись ним. Здається, що близько 80% публікацій, про які я читав, ти ділишся чудовими маленькими діамантами знань у коментарях, які мають виходити з величезного океану досвіду. Дуже дякую!
Бретт Холман

Відповіді:


520

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 з очікуванням (2).
Лука Теттаманті

7
Ах, я бачу - інша інтерпретація :) Я читав питання, що означає "повернути вихідний код 1 негайно, коли будь-який із підпроцессів вийде".
Альнітак

56
PID дійсно може бути повторно використаний, але ви не можете дочекатися процесу, який не є дочірнім поточним процесом (у такому випадку чекати не вдається).
хтокошка

12
Ви також можете використовувати% n для позначення n: th фонового завдання, а %% для позначення останнього.
conny

30
@Nils_M: Ти маєш рацію, пробач. Так це було б щось на кшталт: for i in $n_procs; do ./procs[${i}] & ; pids[${i}]=$!; done; for pid in ${pids[*]}; do wait $pid; done;правда?
синак

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 -pнадає PIDs підпроцесів, які знаходяться у стані виконання. Він буде пропускати процес, якщо процес закінчується раніше, ніж jobs -pвикликається. Отже, якщо будь-який з підпроцесів закінчується раніше jobs -p, статус виходу цього процесу буде втрачено.
tkokoszka

15
Нічого собі, ця відповідь набагато краща, ніж найкраща. : /
e40

4
@ e40, а відповідь нижче, мабуть, ще краща. І ще краще, мабуть, буде запускати кожну команду з '(cmd; echo "$?" >> "$ tmpfile"), використовувати це зачекання, а потім читати файл для помилок. Також анотація-вихід. … Або просто використовуйте цей скрипт, коли вас так не хвилює.
HoverHell

Я хотів би додати, що ця відповідь є кращою, ніж прийнята
shurikk

2
Точніше @tkokoszka - jobs -pце не надання PIDs підпроцесів, а натомість GPID . Логіка очікування все одно працює, вона завжди чекає на групу, якщо така група існує, і підходить, якщо ні, але це добре бути в курсі. Особливо, якщо на цьому потрібно будувати і включати щось на зразок надсилання повідомлень до підпроцесу, в якому У випадку випадку синтаксис відрізняється залежно від того, чи є у вас PID або GPID .. тобто kill -- -$GPIDпротиkill $PID
Тимо

58

Ось простий приклад використання 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.

1
Для тих, хто не працює в bash, два розрахунки в прикладі тут є sleep 20 && trueі sleep 20 && false- тобто: замініть їх на вашу функцію (и). Щоб зрозуміти &&і ||, запустіть man bashі введіть '/' (пошук), тоді '^ * Списки' (регулярний вираз), тоді введіть: man прокрутить униз до опису &&і||
drevicko

1
ви, мабуть, переконайтеся, що файл "fail" не існує на початку (або видаліть його). Залежно від програми, може також бути хорошою ідеєю додати "2> & 1" перед тим, як і ||для того, щоб зловити STDERR в невдачі.
drevicko

мені подобається ця, якісь недоліки? насправді лише тоді, коли мені хочеться перерахувати всі підпроцеси та вжити деяких дій, наприклад. надішліть сигнал, що я спробую вести бухгалтерію та повторити завдання. Зачекайте фінішу, простоwait
xgwang

У цьому випадку буде пропущено статус виходу з роботи, яка не вдалася до
виклику

50

Якщо у вас встановлений паралельний параметр GNU, ви можете зробити:

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

GNU Paralellel надасть вам код виходу:

  • 0 - Усі роботи виконувались без помилок.

  • 1-253 - Деякі завдання не вдалися. Статус виходу дає кількість невдалих завдань

  • 254 - Більше 253 робочих місць не вдалося.

  • 255 - Інша помилка.

Перегляньте вступні відео, щоб дізнатися більше: http://pi.dk/1


1
Дякую! Але ви забули згадати про проблему "плутанини", до якої я згодом потрапив: unix.stackexchange.com/a/35953
nobar

1
Це виглядає як чудовий інструмент, але я не думаю, що вищезазначене працює як є в сценарії Bash, де doCalculationsфункція, визначена в тому самому сценарії (хоча ОП не була зрозуміла щодо цієї вимоги). Коли я намагаюся, parallelкаже /bin/bash: doCalculations: command not found(це говорить 10 разів на seq 0 9прикладі вище). Дивіться тут для вирішення.
nobar

3
Також цікаво: xargsмає певні можливості для паралельного запуску завдань через -Pопцію. Від сюди : export -f doCalculations ; seq 0 9 |xargs -P 0 -n 1 -I{} bash -c "doCalculations {}". Обмеження xargsперелічено на сторінці man для parallel.
nobar

І якщо doCalculationsпокладається на будь-які інші змінні внутрішнього сценарію (внутрішні PATHтощо), їх, мабуть, потрібно чітко exportредагувати перед запуском parallel.
nobar

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 ...

1
Згідно зі сторінками очікування man, зачекайте, коли декілька 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 або подібне), на відміну від більш прямого підходу до паралелізму Баша .

Ось спрощений приклад роботи ...

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


7

Я не вірю, що це можливо завдяки вбудованому функціоналу Баша.

Ви можете отримувати сповіщення, коли дитина виходить:

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

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

Отримання статусу дитини зазвичай є завданням waitсімейства функцій в нижчих рівнях POSIX API. На жаль, підтримка Баша для цього обмежена - ви можете дочекатися одного конкретного дочірнього процесу (і отримати його статус виходу) або ви можете дочекатися всіх , і завжди отримати 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 + CI, все ще бачу процеси, що працюють у фоновому режимі.
Карстен

2
@karsten - це інша проблема. Якщо припустити, що ви використовуєте bash, ви можете захопити умову виходу (включаючи Ctrl + C) і trap "kill 0" EXIT
Філ

@Phil вірно. Оскільки це фонові процеси, вбивство батьківського процесу просто залишає будь-які дочірні процеси запущеними. Мій приклад не вловлює жодних сигналів, які можна додати за потреби, як заявив Філ.
Джейсон Слобоцький

6

Це те, що я використовую:

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

5

Наступний код буде чекати завершення всіх обчислень та повертати статус виходу 1, якщо будь-яка з докалькуляцій не вдається.

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

5

Ось моя версія, яка працює для декількох під-файлів, реєструє попередження, якщо виконання триває занадто довго, і зупиняє підпроцеси, якщо виконання займає довше заданого значення.

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 або новіша версія, наступне може бути корисним для вас. Він використовує асоціативні масиви для зберігання назв завдань та їх "коду", а також назв завдань та їх підписів. Я також створив простий метод обмеження швидкості, який може стати в нагоді, якщо ваші завдання вимагають багато процесора або часу вводу / виводу, і ви хочете обмежити кількість одночасних завдань.

Сценарій запускає всі завдання в першому циклі і споживає результати у другому.

Це трохи непосильне для простих випадків, але це дозволяє отримати дуже акуратні речі. Наприклад, можна зберігати повідомлення про помилки для кожного завдання в іншому асоціативному масиві та друкувати їх після того, як все впорядкується.

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

Я щойно видозмінював сценарій для фонового зображення і паралелізував процес.

Я провів кілька експериментів (на 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

Ksh:

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

Цей вихід записується на stderr, тому простим рішенням прикладу ОП може бути:

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

Але це не дуже корисніше, ніж tmp-файл IMO. Я не зміг знайти корисний спосіб уникнути tmp-файлу, а також уникнути запуску "зачекати" в нижній частині, яка взагалі не працює.


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

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вихід із ненульовим статусом виходу

на жаль, це не впорається з тим випадком, коли процес у фоновому режимі закінчується ненульовим статусом виходу. (цикл не завершиться негайно. він буде чекати завершення попередніх процесів.)


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

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, кожен раз для мене
inetknght

Ви абсолютно впевнені в цьому? Не впевнений, чи це має сенс.
Олександр Міллс

2

пастка - твій друг. Ви можете захопити ERR у багатьох системах. Ви можете захопити EXIT або DEBUG, щоб виконати фрагмент коду після кожної команди.

Це на додаток до всіх стандартних сигналів.


1
Будь ласка, можете пояснити свою відповідь кількома прикладами.
Mοδε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якщо будь-яка підзадача не вдалася.


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

Список літератури


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.


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

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

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" - це неіснуючий процес, що означає, що дитина могла вийти.


1

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

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).
drevicko

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

Я думаю, що найбільш прямим способом паралельно виконувати завдання та перевіряти стан - це використання тимчасових файлів. Уже є пара подібних відповідей (наприклад, Ніцше-джоу та гурт896).

#!/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 -pдля збору 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.