Parallelisieren Sie das Bash-Skript mit der maximalen Anzahl von Prozessen

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

  •  09-06-2019
  •  | 
  •  

Frage

Nehmen wir an, ich habe eine Schleife in Bash:

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

do-something ist CPU-gebunden und ich habe einen schönen, glänzenden 4-Kern-Prozessor.Ich würde gerne bis zu 4 laufen können do-somethingist auf einmal.

Der naive Ansatz scheint zu sein:

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

Dies wird ausgeführt alle do-somethings auf einmal, aber es gibt ein paar Nachteile, vor allem, dass etwas tun kann auch einige erhebliche I/O-Vorgänge haben, die die Leistung beeinträchtigen alle könnte auf einmal etwas langsamer werden.Das andere Problem besteht darin, dass dieser Codeblock sofort zurückkehrt, sodass keine Möglichkeit besteht, andere Arbeiten auszuführen, wenn alle do-somethings sind fertig.

Wie würden Sie diese Schleife schreiben, damit es immer X gibt? do-somethingläuft auf einmal?

War es hilfreich?

Lösung

Je nachdem, was Sie tun möchten, können auch xargs hilfreich sein (hier:Dokumente mit pdf2ps konvertieren):

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

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

Aus den Dokumenten:

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

Andere Tipps

Mit GNU Parallel http://www.gnu.org/software/parallel/ Du kannst schreiben:

some-command | parallel do-something

GNU Parallel unterstützt auch die Ausführung von Jobs auf Remotecomputern.Dadurch wird einer pro CPU-Kern auf den Remote-Computern ausgeführt – auch wenn diese eine unterschiedliche Anzahl von Kernen haben:

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

Ein fortgeschritteneres Beispiel:Hier listen wir die Dateien auf, auf denen my_script ausgeführt werden soll.Dateien haben die Erweiterung (vielleicht .jpeg).Wir möchten, dass die Ausgabe von my_script neben den Dateien in basename.out platziert wird (z. B.foo.jpeg -> foo.out).Wir möchten my_script einmal für jeden Kern ausführen, über den der Computer verfügt, und wir möchten es auch auf dem lokalen Computer ausführen.Für die Remote-Computer möchten wir, dass die zu verarbeitende Datei auf den angegebenen Computer übertragen wird.Wenn my_script fertig ist, möchten wir foo.out zurückübertragen und dann wollen wir, dass foo.jpeg und foo.out vom Remote-Computer entfernt werden:

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

GNU Parallel stellt sicher, dass sich die Ausgabe der einzelnen Jobs nicht vermischt, sodass Sie die Ausgabe als Eingabe für ein anderes Programm verwenden können:

some-command | parallel do-something | postprocess

Weitere Beispiele finden Sie in den Videos: 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 ...

Verwenden Sie anstelle einer einfachen Bash ein Makefile und geben Sie dann die Anzahl der gleichzeitigen Jobs an make -jX Dabei ist X die Anzahl der gleichzeitig auszuführenden Jobs.

Oder Sie können verwenden wait ("man wait"):Mehrere untergeordnete Prozesse starten, aufrufen wait – Es wird beendet, wenn die untergeordneten Prozesse abgeschlossen sind.

maxjobs = 10

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

job ( ){
...
}

Wenn Sie das Ergebnis des Jobs speichern müssen, weisen Sie dessen Ergebnis einer Variablen zu.Nach wait Sie überprüfen einfach, was die Variable enthält.

Hier eine alternative Lösung, die in .bashrc eingefügt und für alltägliche Einzeiler verwendet werden kann:

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

Um es zu nutzen, muss man es nur einsetzen & nach den Jobs und einem pwait-Aufruf gibt der Parameter die Anzahl der parallelen Prozesse an:

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

Es wäre schöner zu verwenden wait anstatt damit beschäftigt zu sein, auf die Ausgabe von zu warten jobs -p, aber es scheint keine offensichtliche Lösung zu geben, zu warten, bis einer der angegebenen Jobs abgeschlossen ist, anstatt alle.

Versuchen Sie es vielleicht mit einem Parallelisierungsdienstprogramm, anstatt die Schleife neu zu schreiben?Ich bin ein großer Fan von xjobs.Ich verwende xjobs ständig, um Dateien in unserem Netzwerk massenhaft zu kopieren, normalerweise beim Einrichten eines neuen Datenbankservers.http://www.maier-komor.de/xjobs.html

Während ich das direkt mache bash wahrscheinlich unmöglich ist, können Sie ziemlich einfach eine Halbrechtsbewegung durchführen. bstark gab eine ziemlich gute Annäherung an das Recht, weist jedoch die folgenden Mängel auf:

  • Wortaufteilung:Sie können ihm keine Jobs übergeben, deren Argumente eines der folgenden Zeichen verwenden:Leerzeichen, Tabulatoren, Zeilenumbrüche, Sterne, Fragezeichen.Wenn Sie dies tun, werden die Dinge möglicherweise unerwartet kaputt gehen.
  • Es ist darauf angewiesen, dass der Rest Ihres Skripts keine Hintergrundinformationen enthält.Wenn Sie dies tun oder später dem Skript etwas hinzufügen, das im Hintergrund gesendet wird, weil Sie aufgrund seines Snippets vergessen haben, dass Sie keine Hintergrundjobs verwenden dürfen, werden die Dinge kaputt gehen.

Eine weitere Näherung, die diese Mängel nicht aufweist, ist die folgende:

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

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

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

    wait "${pids[@]}"
}

Beachten Sie, dass diese Funktion leicht angepasst werden kann, um auch den Exit-Code jedes Jobs zu überprüfen, wenn dieser beendet wird, sodass Sie den Benutzer warnen können, wenn ein Job fehlschlägt, oder einen Exit-Code festlegen können scheduleAll entsprechend der Anzahl der Jobs, die fehlgeschlagen sind, oder so.

Das Problem mit diesem Code ist genau das:

  • Es plant (in diesem Fall) vier Jobs gleichzeitig und wartet dann, bis alle vier beendet sind.Einige werden möglicherweise früher erledigt als andere, was dazu führt, dass der nächste Stapel von vier Aufträgen wartet, bis der längste Teil des vorherigen Stapels abgeschlossen ist.

Es müsste eine Lösung verwendet werden, die dieses letzte Problem behebt kill -0 um abzufragen, ob einer der Prozesse statt der verschwunden ist wait und den nächsten Auftrag planen.Allerdings bringt das ein kleines neues Problem mit sich:Es besteht eine Wettlaufbedingung zwischen dem Ende eines Jobs und dem kill -0 Überprüfen, ob es beendet ist.Wenn der Job beendet wurde und gleichzeitig ein anderer Prozess auf Ihrem System gestartet wird, wird eine zufällige PID verwendet, die zufällig die des Jobs ist, der gerade beendet wurde kill -0 Sie werden nicht merken, dass Ihre Arbeit erledigt ist, und die Dinge werden wieder kaputt gehen.

Eine perfekte Lösung ist hier nicht möglich bash.

Wenn Sie mit dem vertraut sind make In den meisten Fällen können Sie die Liste der Befehle, die Sie ausführen möchten, als Makefile ausdrücken.Wenn Sie beispielsweise $SOME_COMMAND für Dateien *.input ausführen müssen, die jeweils *.output erzeugen, können Sie das Makefile verwenden

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

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

all: $(OUTPUT)

und dann einfach laufen

make -j<NUMBER>

um höchstens ANZAHL Befehle parallel auszuführen.

Funktion für Bash:

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
}

mit:

cat my_commands | parallel -j 4

Das Projekt, an dem ich arbeite, verwendet die Warten Befehl zur Steuerung paralleler Shell-Prozesse (eigentlich KSH).Um Ihre Bedenken hinsichtlich E/A auszuräumen: Auf einem modernen Betriebssystem ist es möglich, dass die parallele Ausführung tatsächlich die Effizienz steigert.Wenn alle Prozesse dieselben Blöcke auf der Festplatte lesen, muss nur der erste Prozess auf die physische Hardware zugreifen.Die anderen Prozesse sind häufig in der Lage, den Block aus dem Festplatten-Cache des Betriebssystems im Speicher abzurufen.Offensichtlich ist das Lesen aus dem Speicher um mehrere Größenordnungen schneller als das Lesen von der Festplatte.Außerdem erfordert der Vorteil keine Codierungsänderungen.

Dies mag für die meisten Zwecke ausreichend sein, ist jedoch nicht optimal.

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

Wirklich Ich komme zu spät zur Party, aber hier ist eine andere Lösung.

Viele Lösungen verarbeiten keine Leerzeichen/Sonderzeichen in den Befehlen, halten N-Jobs nicht ständig am Laufen, verbrauchen CPU in Auslastungsschleifen oder verlassen sich auf externe Abhängigkeiten (z. B.GNU parallel).

Mit Inspiration für die Handhabung von Tot-/Zombie-Prozessen, hier ist eine reine Bash-Lösung:

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
}

Und Beispielverwendung:

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[@]}"

Die Ausgabe:

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

Für die Ausgabeverarbeitung pro Prozess $$ könnte zum Protokollieren in einer Datei verwendet werden, zum Beispiel:

function job_done {
    cat "$1.log"
}

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

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

Ausgabe:

1 56871
2 56872

Sie können eine einfache verschachtelte for-Schleife verwenden (ersetzen Sie N und M unten durch entsprechende Ganzzahlen):

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

Dadurch wird do_something N*M Mal in M ​​Runden ausgeführt, wobei jede Runde N Jobs parallel ausführt.Sie können N gleich der Anzahl Ihrer CPUs machen.

So habe ich es geschafft, dieses Problem in einem Bash-Skript zu lösen:

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

Meine Lösung, um immer eine bestimmte Anzahl von Prozessen am Laufen zu halten, Fehler zu verfolgen und unterbrechungsfreie / Zombie-Prozesse zu verwalten:

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
}

Verwendung:

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 = "Liste einiger Domänen in Befehlen" für foo in some-commandTun

eval `some-command for $DOMAINS` &

    job[$i]=$!

    i=$(( i + 1))

Erledigt

Ndomains=echo $DOMAINS |wc -w

for i in $(seq 1 1 $Ndomains) tun echo "warte auf ${job[$i]}" wait "${job[$i]}" fertig

In diesem Konzept wird für die Parallelisierung gearbeitet.Wichtig ist, dass die letzte Zeile von eval '&' ist Dadurch werden die Befehle auf Hintergründe übertragen.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top