Parallelisieren Sie das Bash-Skript mit der maximalen Anzahl von Prozessen
-
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-something
ist auf einmal.
Der naive Ansatz scheint zu sein:
for foo in `some-command`
do
do-something $foo &
done
Dies wird ausgeführt alle do-something
s 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-something
s sind fertig.
Wie würden Sie diese Schleife schreiben, damit es immer X gibt? do-something
läuft auf einmal?
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-command
Tun
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.