موازاة البرنامج النصي Bash مع الحد الأقصى لعدد العمليات

StackOverflow https://stackoverflow.com/questions/38160

  •  09-06-2019
  •  | 
  •  

سؤال

لنفترض أن لدي حلقة في Bash:

for foo in `some-command`
do
   do-something $foo
done

do-something مرتبط بوحدة المعالجة المركزية ولدي معالج رباعي النواة ولامع.أود أن أكون قادرًا على الركض حتى 4 do-somethingفي وقت واحد.

ويبدو أن النهج الساذج هو:

for foo in `some-command`
do
   do-something $foo &
done

سيتم تشغيل هذا الجميع do-somethingيتم ذلك في وقت واحد، ولكن هناك بعض الجوانب السلبية، بشكل رئيسي أن القيام بشيء ما قد يحتوي أيضًا على بعض عمليات الإدخال/الإخراج المهمة التي تؤدي الجميع في وقت واحد قد تبطئ قليلا.المشكلة الأخرى هي أن كتلة التعليمات البرمجية هذه تعود فورًا، لذا لا توجد طريقة للقيام بعمل آخر عندما تكون جميعها do-somethingتم الانتهاء.

كيف تكتب هذه الحلقة بحيث يكون هناك دائمًا X do-somethingيعمل في وقت واحد؟

هل كانت مفيدة؟

المحلول

اعتمادًا على ما تريد القيام به، يمكن أن يساعدك xargs أيضًا (هنا:تحويل المستندات باستخدام pdf2ps):

cpus=$( ls -d /sys/devices/system/cpu/cpu[[:digit:]]* | wc -w )

find . -name \*.pdf | xargs --max-args=1 --max-procs=$cpus  pdf2ps

من المستندات:

--max-procs=max-procs
-P max-procs
       Run up to max-procs processes at a time; the default is 1.
       If max-procs is 0, xargs will run as many processes as  possible  at  a
       time.  Use the -n option with -P; otherwise chances are that only one
       exec will be done.

نصائح أخرى

مع جنو الموازي http://www.gnu.org/software/parallel/ يمكنك كتابة:

some-command | parallel do-something

يدعم GNU Parallel أيضًا تشغيل المهام على أجهزة الكمبيوتر البعيدة.سيؤدي هذا إلى تشغيل واحد لكل نواة وحدة المعالجة المركزية على أجهزة الكمبيوتر البعيدة - حتى لو كان لديها عدد مختلف من النوى:

some-command | parallel -S server1,server2 do-something

مثال أكثر تقدما:نعرض هنا قائمة بالملفات التي نريد تشغيل my_script عليها.الملفات لها امتداد (ربما .jpeg).نريد أن يتم وضع مخرجات my_script بجوار الملفات الموجودة في basename.out (على سبيل المثال.foo.jpeg -> foo.out).نريد تشغيل my_script مرة واحدة لكل نواة بالكمبيوتر ونريد تشغيله على الكمبيوتر المحلي أيضًا.بالنسبة لأجهزة الكمبيوتر البعيدة، نريد أن تتم معالجة الملف ونقله إلى الكمبيوتر المحدد.عند انتهاء my_script، نريد نقل foo.out مرة أخرى ثم نريد إزالة foo.jpeg وfoo.out من الكمبيوتر البعيد:

cat list_of_files | \
parallel --trc {.}.out -S server1,server2,: \
"my_script {} > {.}.out"

يتأكد GNU Parallel من عدم اختلاط المخرجات من كل مهمة، لذا يمكنك استخدام المخرجات كمدخل لبرنامج آخر:

some-command | parallel do-something | postprocess

شاهد مقاطع الفيديو لمزيد من الأمثلة: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1

maxjobs=4
parallelize () {
        while [ $# -gt 0 ] ; do
                jobcnt=(`jobs -p`)
                if [ ${#jobcnt[@]} -lt $maxjobs ] ; then
                        do-something $1 &
                        shift  
                else
                        sleep 1
                fi
        done
        wait
}

parallelize arg1 arg2 "5 args to third job" arg4 ...

بدلاً من bash العادي، استخدم Makefile، ثم حدد عدد المهام المتزامنة معه make -jX حيث X هو عدد المهام التي سيتم تشغيلها في وقت واحد.

أو يمكنك استخدام wait ("man wait"):إطلاق العديد من العمليات الفرعية، اتصل wait - سيتم الخروج عند انتهاء العمليات الفرعية.

maxjobs = 10

foreach line in `cat file.txt` {
 jobsrunning = 0
 while jobsrunning < maxjobs {
  do job &
  jobsrunning += 1
 }
wait
}

job ( ){
...
}

إذا كنت بحاجة إلى تخزين نتيجة المهمة، فقم بتعيين النتيجة إلى متغير.بعد wait عليك فقط التحقق مما يحتويه المتغير.

هنا حل بديل يمكن إدراجه في .bashrc واستخدامه للبطانة اليومية:

function pwait() {
    while [ $(jobs -p | wc -l) -ge $1 ]; do
        sleep 1
    done
}

لاستخدامه، كل ما عليك فعله هو وضعه & بعد المهام واستدعاء pwait، تعطي المعلمة عدد العمليات المتوازية:

for i in *; do
    do_something $i &
    pwait 10
done

سيكون أجمل للاستخدام wait بدلا من الانشغال في انتظار إخراج jobs -p, ، ولكن لا يبدو أن هناك حلًا واضحًا للانتظار حتى يتم الانتهاء من أي من المهام المحددة بدلاً من الانتهاء منها جميعًا.

ربما تحاول استخدام أداة موازية بدلاً من إعادة كتابة الحلقة؟أنا معجب كبير بـ xjobs.أستخدم xjobs طوال الوقت لنسخ الملفات بشكل جماعي عبر شبكتنا، عادةً عند إعداد خادم قاعدة بيانات جديد.http://www.maier-komor.de/xjobs.html

أثناء القيام بذلك بشكل صحيح bash ربما يكون ذلك مستحيلًا، فيمكنك إجراء نصف اليمين بسهولة إلى حد ما. bstark أعطى تقريبًا عادلاً للحق ولكن به العيوب التالية:

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

التقريب الآخر الذي لا يحتوي على هذه العيوب هو ما يلي:

scheduleAll() {
    local job i=0 max=4 pids=()

    for job; do
        (( ++i % max == 0 )) && {
            wait "${pids[@]}"
            pids=()
        }

        bash -c "$job" & pids+=("$!")
    done

    wait "${pids[@]}"
}

لاحظ أن هذا الخيار قابل للتكيف بسهولة للتحقق أيضًا من رمز الخروج لكل مهمة عند انتهائها حتى تتمكن من تحذير المستخدم في حالة فشل المهمة أو تعيين رمز الخروج لها scheduleAll بحسب كمية الأعمال التي فشلت، أو شيء من هذا القبيل.

المشكلة في هذا الكود هي فقط:

  • فهو يقوم بجدولة أربع مهام (في هذه الحالة) في المرة الواحدة، ثم ينتظر انتهاء المهام الأربع جميعها.قد يتم إنجاز بعض المهام في وقت أقرب من غيرها مما سيؤدي إلى انتظار الدفعة التالية المكونة من أربع مهام حتى تنتهي أطول مهمة من الدفعة السابقة.

يجب استخدام الحل الذي يعتني بهذه المشكلة الأخيرة kill -0 لاستقصاء ما إذا كانت أي من العمليات قد اختفت بدلاً من wait وجدولة المهمة التالية.ومع ذلك، فإن ذلك يقدم مشكلة جديدة صغيرة:لديك حالة سباق بين نهاية الوظيفة، و kill -0 التحقق مما إذا كان قد انتهى.إذا انتهت المهمة وبدأت عملية أخرى على نظامك في نفس الوقت، مع أخذ معرف PID عشوائي والذي يصادف أنه الخاص بالمهمة التي انتهت للتو، kill -0 لن تلاحظ انتهاء عملك وسوف تتعطل الأمور مرة أخرى.

الحل الأمثل غير ممكن في bash.

إذا كنت على دراية make الأمر، يمكنك في معظم الأحيان التعبير عن قائمة الأوامر التي تريد تشغيلها كملف تعريفي.على سبيل المثال، إذا كنت بحاجة إلى تشغيل $SOME_COMMAND على الملفات *.input التي ينتج كل منها *.output، فيمكنك استخدام ملف makefile

INPUT  = a.input b.input
OUTPUT = $(INPUT:.input=.output)

%.output : %.input
    $(SOME_COMMAND) $< $@

all: $(OUTPUT)

ثم قم بتشغيل فقط

make -j<NUMBER>

لتشغيل NUMBER من الأوامر بالتوازي.

وظيفة باش:

parallel ()
{
    awk "BEGIN{print \"all: ALL_TARGETS\\n\"}{print \"TARGET_\"NR\":\\n\\t@-\"\$0\"\\n\"}END{printf \"ALL_TARGETS:\";for(i=1;i<=NR;i++){printf \" TARGET_%d\",i};print\"\\n\"}" | make $@ -f - all
}

استخدام:

cat my_commands | parallel -j 4

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

قد يكون هذا جيدًا بما يكفي لمعظم الأغراض، ولكنه ليس الأمثل.

#!/bin/bash

n=0
maxjobs=10

for i in *.m4a ; do
    # ( DO SOMETHING ) &

    # limit jobs
    if (( $(($((++n)) % $maxjobs)) == 0 )) ; then
        wait # wait until all have finished (not optimal, but most times good enough)
        echo $n wait
    fi
done

حقًا متأخرا للحزب هنا، ولكن هنا حل آخر.

الكثير من الحلول لا تتعامل مع المسافات/الأحرف الخاصة في الأوامر، ولا تبقي وظائف N قيد التشغيل في جميع الأوقات، أو تأكل وحدة المعالجة المركزية في حلقات مشغولة، أو تعتمد على تبعيات خارجية (على سبيل المثال.جنو parallel).

مع إلهام للتعامل مع عملية الموتى/الزومبي, ، إليك حل باش النقي:

function run_parallel_jobs {
    local concurrent_max=$1
    local callback=$2
    local cmds=("${@:3}")
    local jobs=( )

    while [[ "${#cmds[@]}" -gt 0 ]] || [[ "${#jobs[@]}" -gt 0 ]]; do
        while [[ "${#jobs[@]}" -lt $concurrent_max ]] && [[ "${#cmds[@]}" -gt 0 ]]; do
            local cmd="${cmds[0]}"
            cmds=("${cmds[@]:1}")

            bash -c "$cmd" &
            jobs+=($!)
        done

        local job="${jobs[0]}"
        jobs=("${jobs[@]:1}")

        local state="$(ps -p $job -o state= 2>/dev/null)"

        if [[ "$state" == "D" ]] || [[ "$state" == "Z" ]]; then
            $callback $job
        else
            wait $job
            $callback $job $?
        fi
    done
}

واستخدام العينة:

function job_done {
    if [[ $# -lt 2 ]]; then
        echo "PID $1 died unexpectedly"
    else
        echo "PID $1 exited $2"
    fi
}

cmds=( \
    "echo 1; sleep 1; exit 1" \
    "echo 2; sleep 2; exit 2" \
    "echo 3; sleep 3; exit 3" \
    "echo 4; sleep 4; exit 4" \
    "echo 5; sleep 5; exit 5" \
)

# cpus="$(getconf _NPROCESSORS_ONLN)"
cpus=3
run_parallel_jobs $cpus "job_done" "${cmds[@]}"

الإخراج:

1
2
3
PID 56712 exited 1
4
PID 56713 exited 2
5
PID 56714 exited 3
PID 56720 exited 4
PID 56724 exited 5

لمعالجة الإخراج لكل عملية $$ يمكن استخدامها لتسجيل الدخول إلى ملف، على سبيل المثال:

function job_done {
    cat "$1.log"
}

cmds=( \
    "echo 1 \$\$ >\$\$.log" \
    "echo 2 \$\$ >\$\$.log" \
)

run_parallel_jobs 2 "job_done" "${cmds[@]}"

انتاج:

1 56871
2 56872

يمكنك استخدام حلقة for متداخلة بسيطة (استبدل الأعداد الصحيحة المناسبة بـ N وM أدناه):

for i in {1..N}; do
  (for j in {1..M}; do do_something; done & );
done

سيؤدي هذا إلى تنفيذ do_something N*M مرات في جولات M، كل جولة تنفذ وظائف N بالتوازي.يمكنك جعل N يساوي عدد وحدات المعالجة المركزية لديك.

إليك كيف تمكنت من حل هذه المشكلة في برنامج نصي bash:

 #! /bin/bash

 MAX_JOBS=32

 FILE_LIST=($(cat ${1}))

 echo Length ${#FILE_LIST[@]}

 for ((INDEX=0; INDEX < ${#FILE_LIST[@]}; INDEX=$((${INDEX}+${MAX_JOBS})) ));
 do
     JOBS_RUNNING=0
     while ((JOBS_RUNNING < MAX_JOBS))
     do
         I=$((${INDEX}+${JOBS_RUNNING}))
         FILE=${FILE_LIST[${I}]}
         if [ "$FILE" != "" ];then
             echo $JOBS_RUNNING $FILE
             ./M22Checker ${FILE} &
         else
             echo $JOBS_RUNNING NULL &
         fi
         JOBS_RUNNING=$((JOBS_RUNNING+1))
     done
     wait
 done

الحل الذي أقترحه هو الحفاظ دائمًا على تشغيل عدد معين من العمليات، ومتابعة الأخطاء والتعامل مع العمليات غير القابلة للمقاطعة/الزومبي:

function log {
    echo "$1"
}

# Take a list of commands to run, runs them sequentially with numberOfProcesses commands simultaneously runs
# Returns the number of non zero exit codes from commands
function ParallelExec {
    local numberOfProcesses="${1}" # Number of simultaneous commands to run
    local commandsArg="${2}" # Semi-colon separated list of commands

    local pid
    local runningPids=0
    local counter=0
    local commandsArray
    local pidsArray
    local newPidsArray
    local retval
    local retvalAll=0
    local pidState
    local commandsArrayPid

    IFS=';' read -r -a commandsArray <<< "$commandsArg"

    log "Runnning ${#commandsArray[@]} commands in $numberOfProcesses simultaneous processes."

    while [ $counter -lt "${#commandsArray[@]}" ] || [ ${#pidsArray[@]} -gt 0 ]; do

        while [ $counter -lt "${#commandsArray[@]}" ] && [ ${#pidsArray[@]} -lt $numberOfProcesses ]; do
            log "Running command [${commandsArray[$counter]}]."
            eval "${commandsArray[$counter]}" &
            pid=$!
            pidsArray+=($pid)
            commandsArrayPid[$pid]="${commandsArray[$counter]}"
            counter=$((counter+1))
        done


        newPidsArray=()
        for pid in "${pidsArray[@]}"; do
            # Handle uninterruptible sleep state or zombies by ommiting them from running process array (How to kill that is already dead ? :)
            if kill -0 $pid > /dev/null 2>&1; then
                pidState=$(ps -p$pid -o state= 2 > /dev/null)
                if [ "$pidState" != "D" ] && [ "$pidState" != "Z" ]; then
                    newPidsArray+=($pid)
                fi
            else
                # pid is dead, get it's exit code from wait command
                wait $pid
                retval=$?
                if [ $retval -ne 0 ]; then
                    log "Command [${commandsArrayPid[$pid]}] failed with exit code [$retval]."
                    retvalAll=$((retvalAll+1))
                fi
            fi
        done
        pidsArray=("${newPidsArray[@]}")

        # Add a trivial sleep time so bash won't eat all CPU
        sleep .05
    done

    return $retvalAll
}

الاستخدام:

cmds="du -csh /var;du -csh /tmp;sleep 3;du -csh /root;sleep 10; du -csh /home"

# Execute 2 processes at a time
ParallelExec 2 "$cmds"

# Execute 4 processes at a time
ParallelExec 4 "$cmds"

$ domains = "قائمة ببعض المجالات في الأوامر" لـ Foo In some-commandيفعل

eval `some-command for $DOMAINS` &

    job[$i]=$!

    i=$(( i + 1))

منتهي

ندومينز=echo $DOMAINS |wc -w

لأني في $ (seq 1 1 $ ndomains) هل صدى "انتظر $ {Job [$ i]}" انتظر "$ {Job [$ i]}"

في هذا المفهوم سوف تعمل على التوازي.الشيء المهم هو السطر الأخير من Eval هو "&" الذي سيضع الأوامر على الخلفيات.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top