كيفية الانتظار في bash للعديد من subprocesses لإنهاء وإرجاع رمز الإنهاء!=0 عندما ينتهي أي subprocess برمز!=0؟




wait (16)

كيفية الانتظار في البرنامج النصي bash للعديد من subprocesses spawned من هذا البرنامج النصي لإنهاء وإرجاع رمز الإنهاء! = 0 عندما ينتهي أي من subprocesses مع التعليمات البرمجية! = 0؟

نص بسيط:

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

سينتظر النص أعلاه للحصول على كافة العمليات الفرعية 10 الناتجة ، ولكنه دائمًا سيعطي حالة الخروج 0 (انظر help wait ). كيف يمكنني تعديل هذا البرنامج النصي بحيث يكتشف حالات الخروج من subprocesses spawned وإرجاع رمز الإنهاء 1 عند انتهاء أي من subprocesses مع التعليمات البرمجية! = 0؟

هل هناك أي حل أفضل لذلك من جمع PIDs من العمليات الفرعية ، انتظر منهم في النظام وحالات خروج المجموع؟


أرى الكثير من الأمثلة الجيدة المدرجة هنا ، أراد أن يرمي لي أيضا.

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

أستخدم شيئًا مشابهًا للغاية لخوادم بدء / إيقاف التشغيل / الخدمات بالتوازي وتحقق من كل حالة خروج. يعمل بشكل رائع بالنسبة لي. آمل أن يساعد هذا شخص ما!


أنا أفكر ربما تشغيل doCalculations. صدى "$"؟ >> / tmp / acc في subshell يتم إرسالها إلى الخلفية ، ثم الانتظار ، ثم / tmp / acc قد تحتوي على حالات الخروج ، واحد لكل سطر. أنا لا أعرف عن أي عواقب العمليات المتعددة إلحاق ملف تراكم ، رغم ذلك.

إليك تجربة هذا الاقتراح:

ملف: doCalcualtions

#!/bin/sh

random -e 20
sleep $?
random -e 10

الملف: حاول

#!/bin/sh

rm /tmp/acc

for i in $( seq 0 20 ) 
do
        ( ./doCalculations "$i"; echo "$?" >>/tmp/acc ) &
done

wait

cat /tmp/acc | fmt
rm /tmp/acc

خرج تشغيل ./try

5 1 9 6 8 1 2 0 9 6 5 9 6 0 0 4 9 5 5 9 8

إذا كان لديك 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

اعتدت هذا مؤخرا (بفضل النتاك):

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


فخ هو صديقك. يمكنك فخ على ERR في الكثير من النظم. يمكنك تعويض EXIT أو على DEBUG لتنفيذ جزء من التعليمات البرمجية بعد كل أمر.

هذا بالإضافة إلى جميع الإشارات القياسية.


فقط قم بتخزين النتائج من القشرة ، على سبيل المثال في ملف.

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

كنت في حاجة إلى ذلك ، ولكن لم تكن العملية المستهدفة طفلاً من الصدفة الحالية ، وفي هذه الحالة لا wait $PID . لقد وجدت البديل التالي بدلاً من ذلك:

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

يعتمد ذلك على وجود procfs ، والتي قد لا تكون متوفرة (Mac لا يوفرها على سبيل المثال). إذا كنت تستخدم قابلية النقل ، فيمكنك استخدام ذلك بدلاً من ذلك:

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

لا أظن أنه ممكن بفضل وظيفة Bash المدمجة.

يمكنك الحصول على إشعار عند خروج طفل:

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

ومع ذلك لا توجد طريقة واضحة للحصول على حالة خروج الطفل في معالج الإشارة.

عادة ما يكون الحصول على حالة الطفل هو مهمة عائلة wait المهام في واجهات برمجة التطبيقات POSIX ذات المستوى الأدنى. لسوء الحظ ، فإن دعم Bash لذلك محدود - يمكنك الانتظار لعملية طفل محددة (والحصول على حالة الخروج) أو يمكنك انتظار كل منهم ، والحصول دائمًا على نتيجة 0.

ما يبدو من المستحيل القيام به هو ما يعادل waitpid(-1) ، الذي يمنع حتى تعود أي عملية طفل.


لقد مررت بهذا وضمنت أفضل الأجزاء من الأمثلة الأخرى هنا. سيقوم هذا البرنامج النصي بتنفيذ وظيفة 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

لموازاة هذا ...

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 ). أعتقد أن استخدام الكلمة الرئيسية يجعل من الأسهل قليلاً رؤية ما يجري.
  • يمكن أن يكون التعامل مع سلسلة Bash مربكًا - لقد وجدت أن استخدام علامات الاقتباس المفردة يعمل بشكل أفضل من أجل التفاف النصوص غير البسيطة.
  • يمكنك بسهولة مقاطعة العملية بأكملها (باستخدام ^ C أو ما شابه) ، على عكس النهج الأكثر مباشرة لتوازيات باش .

إليك مثال عملي مبسط ...

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

هذا العمل ، يجب أن يكون مجرد جيد إن لم يكن أفضل من إجابة @ 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

وبالطبع ، لقد خلدت هذا البرنامج النصي ، في مشروع الآلية الوقائية الوطنية الذي يسمح لك بتشغيل أوامر bash بالتوازي ، وهو مفيد للاختبار:

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


هذا ما أتيت به حتى الآن. أرغب في معرفة كيفية مقاطعة أمر النوم في حالة إنهاء الطفل ، بحيث لا يضطر المرء إلى ضبط 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 "[email protected]"; do
      shift
      if kill -0 "$pid" 2>/dev/null; then
        debug "$pid is still alive."
        set -- "[email protected]" "$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

هناك بالفعل الكثير من الأجوبة هنا ، لكنني مندهش من أن أحدًا قد اقترح استخدام المصفوفات ... لذا إليك ما فعلته - قد يكون هذا مفيدًا لبعض في المستقبل.

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

وإليك الإصدار الخاص بي الذي يعمل لعدة عروض تسعير ، وتحذيرات السجلات إذا استغرق التنفيذ وقتًا طويلاً ، وتوقف العمليات الفرعية إذا استغرق التنفيذ وقتًا أطول من قيمة معينة.

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

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

#!/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 سيتوقف while fg عن التكرار عندما يخرج أي fg مع حالة خروج غير صفرية

للأسف ، لن يتعامل هذا مع الحالة عند إنهاء عملية في الخلفية بحالة خروج غير صفرية. (لن تنتهي الحلقة فورًا. سينتظر اكتمال العمليات السابقة.)







wait