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

86

Nehmen wir an, ich habe eine Schleife in Bash:

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

do-somethingist CPU-gebunden und ich habe einen schönen glänzenden 4-Kern-Prozessor. Ich möchte in der Lage sein, bis zu 4 Sekunden gleichzeitig zu laufen do-something.

Der naive Ansatz scheint zu sein:

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

Das wird laufen alle do-something s auf einmal, aber es gibt ein paar Nachteile, vor allem das tun-was vielleicht auch einige bedeutende I / O haben , die Durchführung alle auf einmal vielleicht ein bisschen langsamer. Das andere Problem ist, dass dieser Codeblock sofort zurückgegeben wird, sodass keine andere Arbeit mehr möglich ist, wenn alle do-somethings fertig sind.

Wie würden Sie diese Schleife schreiben, damit immer X gleichzeitig do-somethinglaufen?

thelsdj
quelle
2
Als Nebenknoten habe ich davon geträumt, der Bash für primitive die Option make's -j hinzuzufügen. Es würde nicht immer funktionieren, aber für einige einfache Fälle, in denen Sie wissen, dass der Körper der Schleife für jede Iteration etwas Einzigartiges tun wird, wäre es ziemlich sauber, einfach "für -j 4 ..." zu sagen.
Entspannen Sie
1
Querverweis auf stackoverflow.com/questions/1537956/… für eine Bash-Lösung, die Leistungsprobleme mindert und Gruppen von Unterprozessen ermöglicht, die getrennt gehalten werden.
Paxdiablo
1
Ich würde meine Lösung stackoverflow.com/a/28965927/340581
Tuttle

Antworten:

61

Je nachdem, was Sie tun möchten, kann xargs auch helfen (hier: Konvertieren von Dokumenten mit pdf2ps):

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.
Fritz G. Mehner
quelle
9
Diese Methode ist meiner Meinung nach die eleganteste Lösung. Außer, da ich paranoid bin, benutze ich immer gerne find [...] -print0und xargs -0.
Amphetamachine
7
cpus=$(getconf _NPROCESSORS_ONLN)
mr.spuratic
1
Warum nicht aus dem Handbuch verwenden --max-procs=0, um so viele Prozesse wie möglich zu erhalten?
EverythingRightPlace
@EverythingRightPlace, die Frage fragt explizit nach nicht mehr Prozessen als verfügbaren Prozessoren. --max-procs=0ist eher wie der Versuch des Fragestellers (starten Sie so viele Prozesse wie Argumente).
Toby Speight
38

Mit GNU Parallel http://www.gnu.org/software/parallel/ können Sie schreiben:

some-command | parallel do-something

GNU Parallel unterstützt auch das Ausführen von Jobs auf Remotecomputern. Auf den Remotecomputern wird eine pro CPU-Kern ausgeführt - auch wenn sie 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 (möglicherweise .jpeg). Wir möchten, dass die Ausgabe von my_script neben den Dateien in basename.out (z. B. foo.jpeg -> foo.out) platziert wird. Wir möchten my_script einmal für jeden Kern des Computers ausführen und es auch auf dem lokalen Computer ausführen. Für die Remotecomputer soll die zu verarbeitende Datei auf den angegebenen Computer übertragen werden. Wenn my_script fertig ist, möchten wir, dass foo.out zurück übertragen wird und dass foo.jpeg und foo.out vom Remotecomputer entfernt werden:

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

GNU Parallel stellt sicher, dass die Ausgabe von jedem Job nicht gemischt wird, 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

Ole Tange
quelle
1
Beachten Sie, dass dies sehr nützlich ist, wenn Sie einen findBefehl zum Generieren einer Dateiliste verwenden, da dies nicht nur das Problem verhindert, wenn in einem Dateinamen ein Leerzeichen vorhanden ist, for i in ...; dosondern auch find, find -name \*.extension1 -or -name \*.extension2was GNU parallel {.} Sehr gut verarbeiten kann.
Leo Izen
Plus 1, obwohl das catnatürlich nutzlos ist.
Tripleee
@tripleee Re: Nutzloser Gebrauch von Katze. Siehe oletange.blogspot.dk/2013/10/useless-use-of-cat.html
Ole Tange
Oh, du bist es! Könnten Sie übrigens den Link in diesem Blog aktualisieren? Der Speicherort von partmaps.org ist leider tot, aber der Iki-Redirector sollte weiterhin funktionieren.
Tripleee
22
maxjobs = 4
parallelize () {
        während [$ # -gt 0]; machen
                jobcnt = (`jobs -p`)
                if [$ {# jobcnt [@]} -lt $ maxjobs]; dann
                        etwas tun $ 1 &
                        Verschiebung  
                sonst
                        Schlaf 1
                fi
        getan
        warten
}}

parallelisieren arg1 arg2 "5 args zum dritten job" arg4 ...
bstark
quelle
10
Stellen Sie fest, dass hier einige ernsthafte Unterzitate vorliegen, sodass alle Jobs, für die Leerzeichen in Argumenten erforderlich sind, schlecht fehlschlagen. Darüber hinaus frisst dieses Skript Ihre CPU lebendig, während es darauf wartet, dass einige Jobs beendet werden, wenn mehr Jobs angefordert werden, als maxjobs zulässt.
lhunath
1
Beachten Sie auch, dass dies voraussetzt, dass Ihr Skript nichts anderes mit Jobs zu tun hat. Wenn Sie es sind, werden diese auch für Maxjobs gezählt.
lhunath
1
Möglicherweise möchten Sie "jobs -pr" verwenden, um die Ausführung von Jobs zu beschränken.
Amphetamachine
1
Es wurde ein Schlafbefehl hinzugefügt, um zu verhindern, dass sich die while-Schleife ohne Unterbrechung wiederholt, während darauf gewartet wird, dass bereits ausgeführte Do-Something-Befehle beendet werden. Andernfalls würde diese Schleife im Wesentlichen einen der CPU-Kerne belegen. Dies spricht auch das Anliegen von @lhunath an.
Euphorie83
12

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

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

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

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

Es wäre besser zu verwenden, als waitauf die Ausgabe von jobs -pzu warten, aber es scheint keine offensichtliche Lösung zu geben, zu warten, bis einer der angegebenen Jobs beendet ist, anstatt alle.

Grumbel
quelle
11

Verwenden Sie anstelle einer einfachen Bash ein Makefile und geben Sie dann die Anzahl der gleichzeitigen Jobs an, make -jXwobei X die Anzahl der Jobs ist, die gleichzeitig ausgeführt werden sollen.

Oder Sie können wait(" man wait") verwenden: mehrere untergeordnete Prozesse starten, aufrufen wait- es wird beendet, wenn die untergeordneten Prozesse beendet 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 das Ergebnis einer Variablen zu. Nachdem waitSie nur überprüft haben, was die Variable enthält.

Skolima
quelle
1
Vielen Dank dafür, obwohl der Code noch nicht fertig ist, gibt er mir die Antwort auf ein Problem, das ich bei der Arbeit habe.
Gerikson
Das einzige Problem ist, dass, wenn Sie das Vordergrundskript (das mit der Schleife) beenden, die ausgeführten Jobs nicht zusammen beendet werden
Girardi
8

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

Tessein
quelle
6

Während Sie dies richtig machen bash es wahrscheinlich unmöglich ist, , können Sie ein Semi-Recht ziemlich einfach machen. bstarkgab eine faire Annäherung an das Recht, aber seine hat die folgenden Mängel:

  • Wortaufteilung: Sie können keine Jobs übergeben, die in ihren Argumenten eines der folgenden Zeichen verwenden: Leerzeichen, Tabulatoren, Zeilenumbrüche, Sterne, Fragezeichen. Wenn Sie dies tun, werden die Dinge möglicherweise unerwartet kaputt gehen.
  • Es hängt vom Rest Ihres Skripts ab, nichts im Hintergrund zu haben. Wenn Sie dies tun oder später dem Skript etwas hinzufügen, das im Hintergrund gesendet wird, weil Sie vergessen haben, dass Sie aufgrund seines Snippets keine Jobs im Hintergrund verwenden dürfen, werden die Dinge kaputt gehen.

Eine andere Annä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 dieser leicht anpassbar ist, um auch den Beendigungscode jedes Jobs am Ende zu überprüfen, damit Sie den Benutzer warnen können, wenn ein Job fehlschlägt, oder einen Beendigungscode für festlegen können scheduleAll die Anzahl der fehlgeschlagenen Jobs .

Das Problem mit diesem Code ist genau das:

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

Eine Lösung, die sich um dieses letzte Problem kümmert, müsste verwenden, kill -0um abzufragen, ob einer der Prozesse anstelle des verschwunden ist, waitund den nächsten Job planen. Dies führt jedoch zu einem kleinen neuen Problem: Sie haben eine Race-Bedingung zwischen dem Beenden eines Jobs und der kill -0Überprüfung, 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 gerade beendeten Jobs istkill -0 nicht bemerkt und die Dinge werden erneut unterbrochen.

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

lhunath
quelle
6

Wenn Sie mit dem vertraut sind make Befehl , können Sie die Liste der Befehle, die Sie ausführen möchten, meistens als Makefile ausdrücken. Wenn Sie beispielsweise $ SOME_COMMAND für Dateien * .input ausführen müssen, von denen jede * .output erzeugt, können Sie das Makefile verwenden

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

%.Ausgang Eingang
    $ (SOME_COMMAND) $ <$ @

alle: $ (AUSGABE)

und dann einfach rennen

make -j <NUMMER>

höchstens NUMBER-Befehle parallel ausführen.

Idelic
quelle
3

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
ilnar
quelle
Die Verwendung von make -jist klug, aber ohne Erklärung und diesen Klumpen von Nur-Schreib-Awk-Code verzichte ich auf Upvoting.
Tripleee
2

Das Projekt, an dem ich arbeite, verwendet den Befehl wait , um parallele Shell-Prozesse (tatsächlich ksh) zu steuern. Um Ihre Bedenken hinsichtlich E / A auf einem modernen Betriebssystem auszuräumen, ist es möglich, dass die parallele Ausführung die Effizienz tatsächlich erhöht. Wenn alle Prozesse dieselben Blöcke auf der Festplatte lesen, muss nur der erste Prozess die physische Hardware treffen. Die anderen Prozesse können den Block häufig aus dem Festplatten-Cache des Betriebssystems im Speicher abrufen. Offensichtlich ist das Lesen aus dem Speicher mehrere Größenordnungen schneller als das Lesen von der Festplatte. Der Vorteil erfordert auch keine Codierungsänderungen.

Jon Ericson
quelle
1

Dies mag für die meisten Zwecke gut genug sein, ist aber 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
Katze
quelle
1

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
Fernando
quelle
1

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

Viele Lösungen verarbeiten keine Leerzeichen / Sonderzeichen in den Befehlen, lassen N Jobs nicht immer laufen, essen CPU in Besetztschleifen oder verlassen sich auf externe Abhängigkeiten (z parallel. B. GNU ).

Mit Inspiration für die Handhabung von Dead / Zombie-Prozessen ist hier 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 Beispielnutzung:

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 prozessübergreifende Ausgabe $$kann die Protokollierung 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
Skrat
quelle
0

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

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

Dies führt N * M-mal in M ​​Runden aus, wobei jede Runde N Jobs parallel ausführt. Sie können N gleich der Anzahl Ihrer CPUs machen.

Adam Zalcman
quelle
0

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

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"
Orsiris de Jong
quelle
-1

$ DOMAINS = "Liste einiger Domänen in Befehlen" für foo in some-command do

eval `some-command for $DOMAINS` &

    job[$i]=$!

    i=$(( i + 1))

getan

Ndomains =echo $DOMAINS |wc -w

für i in $ (seq 1 1 $ Ndomains) echo "warte auf $ {job [$ i]}" warte "$ {job [$ i]}" erledigt

in diesem Konzept wird für die Parallelisierung arbeiten. Wichtig ist, dass die letzte Zeile der Bewertung '&' ist, wodurch die Befehle in den Hintergrund gestellt werden.

Jack
quelle