Parallelisieren Sie eine Bash FOR-Schleife

109

Ich habe versucht, das folgende Skript, insbesondere jede der drei FOR-Schleifeninstanzen, mit GNU Parallel zu parallelisieren, konnte dies jedoch nicht. Die 4 in der FOR-Schleife enthaltenen Befehle werden nacheinander ausgeführt, wobei jede Schleife etwa 10 Minuten dauert.

#!/bin/bash

kar='KAR5'
runList='run2 run3 run4'
mkdir normFunc
for run in $runList
do 
  fsl5.0-flirt -in $kar"deformed.nii.gz" -ref normtemp.nii.gz -omat $run".norm1.mat" -bins 256 -cost corratio -searchrx -90 90 -searchry -90 90 -searchrz -90 90 -dof 12 
  fsl5.0-flirt -in $run".poststats.nii.gz" -ref $kar"deformed.nii.gz" -omat $run".norm2.mat" -bins 256 -cost corratio -searchrx -90 90 -searchry -90 90 -searchrz -90 90 -dof 12 
  fsl5.0-convert_xfm -concat $run".norm1.mat" -omat $run".norm.mat" $run".norm2.mat"
  fsl5.0-flirt -in $run".poststats.nii.gz" -ref normtemp.nii.gz -out $PWD/normFunc/$run".norm.nii.gz" -applyxfm -init $run".norm.mat" -interp trilinear

  rm -f *.mat
done
Ravnoor S Gill
quelle

Antworten:

94

Warum gabelst du sie nicht einfach ab?

foo () {
    local run=$1
    fsl5.0-flirt -in $kar"deformed.nii.gz" -ref normtemp.nii.gz -omat $run".norm1.mat" -bins 256 -cost corratio -searchrx -90 90 -searchry -90 90 -searchrz -90 90 -dof 12 
    fsl5.0-flirt -in $run".poststats.nii.gz" -ref $kar"deformed.nii.gz" -omat $run".norm2.mat" -bins 256 -cost corratio -searchrx -90 90 -searchry -90 90 -searchrz -90 90 -dof 12 
    fsl5.0-convert_xfm -concat $run".norm1.mat" -omat $run".norm.mat" $run".norm2.mat"
    fsl5.0-flirt -in $run".poststats.nii.gz" -ref normtemp.nii.gz -out $PWD/normFunc/$run".norm.nii.gz" -applyxfm -init $run".norm.mat" -interp trilinear
}

for run in $runList; do foo "$run" & done

Falls das nicht klar ist, ist der wesentliche Teil hier:

for run in $runList; do foo "$run" & done
                                   ^

Ausführen der Funktion in einer gegabelten Shell im Hintergrund. Das ist parallel.

Goldlöckchen
quelle
6
Das hat wie ein Zauber gewirkt. Danke. Solch eine einfache Implementierung (macht mich jetzt so dumm!).
Ravnoor S Gill
8
Wenn ich 8 Dateien parallel ausführen müsste, aber nur 4 Kerne, könnte das in eine solche Einstellung integriert werden, oder würde das einen Job Scheduler erfordern?
Ravnoor S Gill
6
In diesem Zusammenhang spielt es keine Rolle. Es ist normal, dass das System mehr aktive Prozesse als Kerne hat. Wenn Sie viele kurze Aufgaben haben , füttern Sie im Idealfall eine Warteschlange, die von einer Anzahl von Worker-Threads <der Anzahl der Kerne bedient wird. Ich weiß nicht, wie oft das wirklich mit Shell-Skripten gemacht wird (in diesem Fall wären es keine Threads, sondern unabhängige Prozesse), aber mit relativ wenigen langen Aufgaben wäre es sinnlos. Der OS-Scheduler kümmert sich um sie.
Goldlöckchen
17
Möglicherweise möchten Sie auch waitam Ende einen Befehl hinzufügen, damit das Masterskript erst beendet wird, wenn alle Hintergrundjobs beendet sind.
Psusi
1
Ich würde es auch als nützlich erachten, die Anzahl der gleichzeitigen Prozesse zu begrenzen: Meine Prozesse beanspruchen jeweils etwa 25 Minuten lang 100% der Kernzeit. Dies ist auf einem gemeinsam genutzten Server mit 16 Kernen, auf dem viele Leute Jobs ausführen. Ich muss 23 Kopien des Skripts ausführen. Wenn ich sie alle gleichzeitig laufen lasse, überflute ich den Server und mache ihn für ein oder zwei Stunden für alle anderen nutzlos (die Last steigt auf 30, alles andere verlangsamt sich erheblich). Ich schätze, es könnte vorbei sein nice, aber dann weiß ich nicht, ob es jemals zu Ende
ginge
150

Beispielaufgabe

task(){
   sleep 0.5; echo "$1";
}

Sequentielle Läufe

for thing in a b c d e f g; do 
   task "$thing"
done

Parallele Läufe

for thing in a b c d e f g; do 
  task "$thing" &
done

Parallele Läufe in N-Prozess-Chargen

N=4
(
for thing in a b c d e f g; do 
   ((i=i%N)); ((i++==0)) && wait
   task "$thing" & 
done
)

Es ist auch möglich, FIFOs als Semaphore zu verwenden, um sicherzustellen, dass neue Prozesse so schnell wie möglich erstellt werden und nicht mehr als N Prozesse gleichzeitig ausgeführt werden. Dafür ist jedoch mehr Code erforderlich.

N Prozesse mit einem FIFO-basierten Semaphor:

open_sem(){
    mkfifo pipe-$$
    exec 3<>pipe-$$
    rm pipe-$$
    local i=$1
    for((;i>0;i--)); do
        printf %s 000 >&3
    done
}
run_with_lock(){
    local x
    read -u 3 -n 3 x && ((0==x)) || exit $x
    (
     ( "$@"; )
    printf '%.3d' $? >&3
    )&
}

N=4
open_sem $N
for thing in {a..g}; do
    run_with_lock task $thing
done 
PSkocik
quelle
4
Die Zeile mit waitdarin lässt im Grunde alle Prozesse laufen, bis sie den nthProzess erreicht, und wartet dann , bis alle anderen beendet sind. Stimmt das?
Naught101
Wenn iNull ist, rufen Sie Wait an. Inkrement inach dem Nulltest.
PSkocik
2
@ naught101 Ja. waitw / no arg wartet auf alle Kinder. Das macht es ein wenig verschwenderisch. Der Pipe-basierte Semaphor-Ansatz bietet eine flüssigere Parallelität (ich verwende dies in einem benutzerdefinierten Shell-basierten Build-System zusammen mit -nt/ -otchecks bereits seit
einiger Zeit
1
@ BeowulfNode42 Du musst nicht beenden. Der Rückgabestatus der Task beeinträchtigt die Konsistenz des Semaphors nicht, solange der Status (oder etwas mit dieser Bytelänge) in das FIFO zurückgeschrieben wird, nachdem der Task-Prozess beendet wurde / abstürzt.
PSkocik
1
Zu Ihrer Information, der mkfifo pipe-$$Befehl benötigt einen entsprechenden Schreibzugriff auf das aktuelle Verzeichnis. Daher ziehe ich es vor, den vollständigen Pfad anzugeben, da /tmp/pipe-$$der aktuelle Benutzer höchstwahrscheinlich über Schreibzugriff verfügt, anstatt sich auf das aktuelle Verzeichnis zu verlassen. Ja, alle 3 Vorkommen von ersetzen pipe-$$.
BeowulfNode42
65
for stuff in things
do
( something
  with
  stuff ) &
done
wait # for all the something with stuff

Ob es tatsächlich funktioniert, hängt von Ihren Befehlen ab. Ich kenne sie nicht. Das rm *.matsieht etwas konfliktanfällig aus, wenn es parallel läuft ...

Frostschutz
quelle
2
Das läuft auch einwandfrei. Sie haben recht, ich müsste rm *.matetwas ändern , rm $run".mat"um es zum Laufen zu bringen, ohne dass ein Prozess den anderen stört. Vielen Dank .
Ravnoor S Gill
@RavnoorSGill Willkommen bei Stack Exchange! Wenn diese Antwort Ihr Problem gelöst hat, markieren Sie es bitte als akzeptiert, indem Sie das Häkchen daneben setzen.
Gilles
7
+1 für waitdie ich vergessen habe.
Goldlöckchen
5
Wenn es Unmengen von Dingen gibt, werden dann nicht Unmengen von Prozessen gestartet? Es wäre besser, nur eine vernünftige Anzahl von Prozessen gleichzeitig zu starten, oder?
David Doria
1
Sehr hilfreicher Tipp! Wie stelle ich in diesem Fall die Anzahl der Threads ein?
Dadong Zhang
30
for stuff in things
do
sem -j+0 ( something
  with
  stuff )
done
sem --wait

Hierbei werden Semaphore verwendet, die so viele Iterationen parallelisieren, wie Kerne verfügbar sind (-j +0 bedeutet, dass Sie N + 0 Jobs parallelisieren , wobei N die Anzahl der verfügbaren Kerne ist ).

sem --wait weist an, zu warten, bis alle Iterationen in der for-Schleife die Ausführung beendet haben, bevor die aufeinanderfolgenden Codezeilen ausgeführt werden.

Hinweis: Sie benötigen "parallel" aus dem GNU-Parallel-Projekt (sudo apt-get install parallel).

lev
quelle
1
ist es möglich, über 60 hinauszugehen? meins wirft einen Fehler mit der Meldung, dass nicht genügend Dateideskriptoren vorhanden sind.
Chovy
Wenn dies auch wegen der geschweiften Klammern einen Syntaxfehler auslöst , werfen Sie einen Blick auf die Antwort von moritzschaefer.
Nicolai
10

Eine wirklich einfache Möglichkeit, die ich oft benutze:

cat "args" | xargs -P $NUM_PARALLEL command

Dadurch wird der Befehl ausgeführt, wobei in jeder Zeile der "args" -Datei gleichzeitig höchstens $ NUM_PARALLEL ausgeführt wird.

Sie können auch die Option -I für xargs prüfen, wenn Sie die Eingabeargumente an verschiedenen Stellen ersetzen müssen.

eyeApps LLC
quelle
6

Es scheint, dass die fsl-Jobs voneinander abhängig sind, sodass die 4 Jobs nicht gleichzeitig ausgeführt werden können. Die Läufe können jedoch parallel ausgeführt werden.

Erstellen Sie eine Bash-Funktion, die einen einzelnen Lauf ausführt, und führen Sie diese Funktion parallel aus:

#!/bin/bash

myfunc() {
    run=$1
    kar='KAR5'
    mkdir normFunc
    fsl5.0-flirt -in $kar"deformed.nii.gz" -ref normtemp.nii.gz -omat $run".norm1.mat" -bins 256 -cost corratio -searchrx -90 90 -searchry -90 90 -searchrz -90 90 -dof 12 
    fsl5.0-flirt -in $run".poststats.nii.gz" -ref $kar"deformed.nii.gz" -omat $run".norm2.mat" -bins 256 -cost corratio -searchrx -90 90 -searchry -90 90 -searchrz -90 90 -dof 12 
    fsl5.0-convert_xfm -concat $run".norm1.mat" -omat $run".norm.mat" $run".norm2.mat"
    fsl5.0-flirt -in $run".poststats.nii.gz" -ref normtemp.nii.gz -out $PWD/normFunc/$run".norm.nii.gz" -applyxfm -init $run".norm.mat" -interp trilinear
}

export -f myfunc
parallel myfunc ::: run2 run3 run4

Weitere Informationen finden Sie in den Introvideos: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1. Gehen Sie eine Stunde lang durch das Tutorial http://www.gnu.org/software/parallel/parallel_tutorial.html Ihr Befehl line wird dich dafür lieben.

Ole Tange
quelle
Wenn Sie eine Nicht-Bash-Shell verwenden, müssen Sie diese auch ausführen, export SHELL=/bin/bashbevor Sie sie parallel ausführen können. Andernfalls erhalten Sie eine Fehlermeldung wie:Unknown command 'myfunc arg'
AndrewHarvey
1
@ Andrew Harvey: Ist es nicht das, wofür der Shebang ist?
Naught101
5

Parallele Ausführung im max. N-Prozess gleichzeitig

#!/bin/bash

N=4

for i in {a..z}; do
    (
        # .. do your stuff here
        echo "starting task $i.."
        sleep $(( (RANDOM % 3) + 1))
    ) &

    # allow only to execute $N jobs in parallel
    if [[ $(jobs -r -p | wc -l) -gt $N ]]; then
        # wait only for first job
        wait -n
    fi

done

# wait for pending jobs
wait

echo "all done"
Tomasz Hławiczka
quelle
3

Die Antwort von @lev gefällt mir sehr gut, da sie auf sehr einfache Weise die Kontrolle über die maximale Anzahl von Prozessen ermöglicht. Wie im Handbuch beschrieben , funktioniert sem jedoch nicht mit Klammern.

for stuff in things
do
sem -j +0 "something; \
  with; \
  stuff"
done
sem --wait

Macht den Job.

-j + N Addiert N zur Anzahl der CPU-Kerne. Führen Sie so viele Jobs gleichzeitig aus. Für rechenintensive Jobs ist -j +0 nützlich, da gleichzeitig Aufträge mit einer Anzahl von CPU-Kernen ausgeführt werden.

-j -N Subtrahiere N von der Anzahl der CPU-Kerne. Führen Sie so viele Jobs gleichzeitig aus. Wenn die bewertete Zahl kleiner als 1 ist, wird 1 verwendet. Siehe auch --use-cpus-anstatt-Kerne.

moritzschäfer
quelle
1

In meinem Fall kann ich kein Semaphor verwenden (ich bin in Git-Bash unter Windows), daher habe ich eine generische Methode gefunden, um die Aufgabe auf N Worker aufzuteilen, bevor sie beginnen.

Es funktioniert gut, wenn die Aufgaben ungefähr die gleiche Zeit in Anspruch nehmen. Der Nachteil ist, dass die anderen, die bereits fertig sind, nicht helfen, wenn einer der Arbeiter viel Zeit benötigt, um seinen Teil der Arbeit zu erledigen.

Aufteilung des Jobs auf N Arbeiter (1 pro Kern)

# array of assets, assuming at least 1 item exists
listAssets=( {a..z} ) # example: a b c d .. z
# listAssets=( ~/"path with spaces/"*.txt ) # could be file paths

# replace with your task
task() { # $1 = idWorker, $2 = asset
  echo "Worker $1: Asset '$2' START!"
  # simulating a task that randomly takes 3-6 seconds
  sleep $(( ($RANDOM % 4) + 3 ))
  echo "    Worker $1: Asset '$2' OK!"
}

nVirtualCores=$(nproc --all)
nWorkers=$(( $nVirtualCores * 1 )) # I want 1 process per core

worker() { # $1 = idWorker
  echo "Worker $1 GO!"
  idAsset=0
  for asset in "${listAssets[@]}"; do
    # split assets among workers (using modulo); each worker will go through
    # the list and select the asset only if it belongs to that worker
    (( idAsset % nWorkers == $1 )) && task $1 "$asset"
    (( idAsset++ ))
  done
  echo "    Worker $1 ALL DONE!"
}

for (( idWorker=0; idWorker<nWorkers; idWorker++ )); do
  # start workers in parallel, use 1 process for each
  worker $idWorker &
done
wait # until all workers are done
Geekley
quelle
0

Ich hatte Probleme mit @PSkocikder Lösung. Mein System verfügt nicht über GNU Parallel als Paket und hat semeine Ausnahme ausgelöst, als ich es manuell erstellt und ausgeführt habe. Ich habe dann auch das FIFO-Semaphor-Beispiel ausprobiert, das auch einige andere Kommunikationsfehler aufwies.

@eyeApps schlug xargs vor, aber ich wusste nicht, wie ich es mit meinem komplexen Anwendungsfall zum Laufen bringen sollte (Beispiele wären willkommen).

Hier ist meine Lösung für parallele Jobs, die bis zu NJobs gleichzeitig verarbeiten, wie konfiguriert von _jobs_set_max_parallel:

_lib_jobs.sh:

function _jobs_get_count_e {
   jobs -r | wc -l | tr -d " "
}

function _jobs_set_max_parallel {
   g_jobs_max_jobs=$1
}

function _jobs_get_max_parallel_e {
   [[ $g_jobs_max_jobs ]] && {
      echo $g_jobs_max_jobs

      echo 0
   }

   echo 1
}

function _jobs_is_parallel_available_r() {
   (( $(_jobs_get_count_e) < $g_jobs_max_jobs )) &&
      return 0

   return 1
}

function _jobs_wait_parallel() {
   # Sleep between available jobs
   while true; do
      _jobs_is_parallel_available_r &&
         break

      sleep 0.1s
   done
}

function _jobs_wait() {
   wait
}

Anwendungsbeispiel:

#!/bin/bash

source "_lib_jobs.sh"

_jobs_set_max_parallel 3

# Run 10 jobs in parallel with varying amounts of work
for a in {1..10}; do
   _jobs_wait_parallel

   # Sleep between 1-2 seconds to simulate busy work
   sleep_delay=$(echo "scale=1; $(shuf -i 10-20 -n 1)/10" | bc -l)

   ( ### ASYNC
   echo $a
   sleep ${sleep_delay}s
   ) &
done

# Visualize jobs
while true; do
   n_jobs=$(_jobs_get_count_e)

   [[ $n_jobs = 0 ]] &&
      break

   sleep 0.1s
done
Zhro
quelle