2008-09-01 6 views
75

Lets sagen, dass ich eine Schleife in Bash haben:parallelisieren Bash-Skript mit der maximalen Anzahl von Prozessen

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

do-something CPU gebunden ist, und ich habe einen schönen glänzenden 4-Core-Prozessor. Ich möchte in der Lage sein, bis zu 4 do-something auf einmal zu laufen.

Der naive Ansatz scheint zu sein:

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

Diese alledo-something s auf einmal laufen, aber es gibt ein paar Nachteile, vor allem, dass etwas tun-können auch einige bedeutende I/O haben die Durchführung alle auf einmal könnte etwas verlangsamen. Das andere Problem besteht darin, dass dieser Codeblock sofort zurückkehrt, so dass keine andere Arbeit ausgeführt werden kann, wenn alle do-something s beendet sind. Wie würden Sie diese Schleife schreiben, so dass immer X do-something s auf einmal läuft?

+2

Als sidenode habe ich die Zugabe von Make Option -j auf den bas geträumt h für primitiv. Es würde nicht immer funktionieren, aber für einige einfache Fälle, wo Sie wissen, dass der Körper der Schleife für jede Iteration etwas Einzigartiges tun wird, wäre es ziemlich sauber, nur "für -j 4 ..." zu sagen. – unwind

+1

Querverweis zu http://stackoverflow.com/questions/1537956/bash-limit-the-number-of-concurrent-jobs/1685440#1685440 für eine Bash-Lösung, die Leistungsprobleme mildert und Gruppen von Unterprozessen ermöglicht getrennt gehalten. – paxdiablo

+1

Ich würde meine Lösung empfehlen http://Stackoverflow.com/a/28965927/340581 – Tuttle

Antwort

50

Je nachdem, was Sie wollen auch helfen können, xargs tun (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 der Dokumentation:

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

Diese Methode ist meiner Meinung nach die eleganteste Lösung. Außer, da ich paranoid bin, benutze ich immer 'find [...] -print0' und' xargs -0'. – amphetamachine

+7

'cpus = $ (getconf _NPROCESSORS_ONLN)' –

+1

Aus dem Handbuch, warum nicht '--max-procs = 0' verwenden, um so viele Prozesse wie möglich zu bekommen? – EverythingRightPlace

10

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

Oder Sie können wait ("man wait") verwenden: Starten Sie mehrere untergeordnete Prozesse, rufen Sie 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 das Ergebnis einer Variablen zu. Nach wait überprüfen Sie einfach, was die Variable enthält.

+1

Danke dafür, obwohl der Code nicht fertig ist, hat es mir die Antwort auf ein Problem, das ich bei der Arbeit habe. – gerikson

8

Vielleicht versuchen Sie ein Parallelisierungs-Dienstprogramm statt die Schleife neu schreiben? Ich bin ein großer Fan von xjobs. Ich verwende xjobs die ganze Zeit, um Dateien in unserem Netzwerk zu kopieren, normalerweise wenn ein neuer Datenbankserver eingerichtet wird. http://www.maier-komor.de/xjobs.html

22
 
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 ... 
+10

Erkennen Sie, dass hier einige schwerwiegende Unterquotierungen auftreten, so dass alle Jobs, die Leerzeichen in Argumenten erfordern, schlecht ablaufen; Darüber hinaus wird dieses Skript Ihre CPU am Leben erhalten, 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 davon ausgeht, dass Ihr Skript nichts anderes mit Jobs zu tun hat; Wenn du es bist, zählt es auch die Maxjobs. – lhunath

+1

Sie können "jobs -pr" verwenden, um auf laufende Jobs zu beschränken. – amphetamachine

2

Das Projekt arbeiten I verwendet der Befehl warten parallel Schale steuern (KSH tatsächlich) -Prozesse. Um Ihre Bedenken hinsichtlich IO auf einem modernen Betriebssystem zu berücksichtigen, ist es möglich, dass die parallele Ausführung die Effizienz erhöht. Wenn alle Prozesse die gleichen 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 Platte. Außerdem erfordert der Vorteil keine Kodierungsänderungen.

8

Hier eine alternative Lösung, die in Bashrc eingesetzt werden kann und für die täglichen einzeiler verwendet:

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

es zu verwenden, alles, was man tun muss, ist gesetzt & nach den Jobs und ein PWAIT Anruf, der Parameter gibt die Anzahl paralleler Prozesse:

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

Es wäre schöner, wait statt busy waiting zu verwenden, um auf den Ausgang von jobs -p, aber es scheint nicht eine offensichtliche Lösung zu warten, bis eine der zu g iven jobs ist beendet, statt alle.

6

Während dies richtig in bash ist wahrscheinlich unmöglich, können Sie ein Semi-Recht ziemlich leicht tun. bstark gab eine gute Annäherung von rechts, aber seine hat folgende Mängel auf:

  • Word-Splitting: Sie können keine Jobs, um es übergeben, die in ihren Argumenten eine der folgenden Zeichen verwenden: Leerzeichen, Tabulatoren, Zeilenumbrüche, Sterne , Fragezeichen. Wenn Sie das tun, werden die Dinge möglicherweise unerwartet brechen.
  • Es beruht auf dem Rest Ihres Skripts, um nichts Hintergrund. Wenn Sie dies tun oder später etwas zum Script hinzufügen, das im Hintergrund gesendet wird, weil Sie vergessen haben, dass Sie aufgrund seines Snippets keine Hintergrundjobs verwenden dürfen, werden die Dinge abbrechen.

Eine andere Annäherung, die nicht diese Fehler hat, 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 dies ein leicht anpassbar ist, die Exit-Code jedes Auftrags auch zu überprüfen, wie es endet, so können Sie die warnen Benutzer, wenn ein Job fehlschlägt, oder setzen Sie einen Exit-Code für scheduleAll entsprechend der Anzahl der Aufträge, die fehlgeschlagen sind, oder etwas.

Das Problem mit diesem Code ist genau das:

  • Es Pläne vier (in diesem Fall) Arbeitsplätze in einer Zeit, und wartet dann auf alle vier zu beenden. Einige können früher als andere ausgeführt werden, was dazu führt, dass der nächste Stapel von vier Jobs wartet, bis der längste des vorherigen Stapels fertig ist.

Eine Lösung, die Pflege dieser letzten Ausgabe nimmt den nächsten Auftrag verwenden, müssten kill -0 abzufragen, ob eine der Prozesse verschwunden sind anstelle der wait und planen. Dies führt jedoch zu einem kleinen neuen Problem: Sie haben eine Wettlaufsituation zwischen einem Job-Ende und der kill -0 Überprüfung, ob es beendet ist. Wenn der Job beendet wird und ein anderer Prozess auf Ihrem System zur gleichen Zeit gestartet wird, nimmt die kill -0 eine zufällige PID, die zufällig die des gerade beendeten Jobs ist, nicht wahr, dass Ihre Arbeit beendet ist und die Dinge wieder kaputt gehen.

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

5

Wenn du mit dem make vertraut sind Befehl, die meiste Zeit können Sie die Liste der Befehle ausdrücken, die Sie als Makefile ausführen möchten. Zum Beispiel müssen, wenn Sie $ some_command auf Dateien * .input von denen jeder produziert * .output, können Sie verwenden, um die Make-Datei

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

%.output : %.input 
    $(SOME_COMMAND) $< [email protected] 

all: $(OUTPUT) 

und führen nur

 
make -j<NUMBER> 

zu laufen höchstens laufen NUMBER Befehle parallel.

34

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

some-command | parallel do-something 

GNU Parallel unterstützt auch Arbeitsplätze auf Remote-Computern ausgeführt wird. Dies wird ein pro CPU-Kern auf den Remote-Computer ausgeführt werden - auch wenn sie unterschiedliche Anzahl der Kerne:

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

Ein fortgeschrittenere Beispiel: Hier listen wir von Dateien, die wir wollen my_script auf laufen. Dateien haben eine Erweiterung (möglicherweise .jpeg). Wir möchten, dass die Ausgabe von my_script neben die Dateien in basename.out gestellt wird (z. B. foo.jpeg -> foo.out). Wir möchten my_script einmal für jeden Core ausführen, den der Computer hat, und wir möchten ihn auch auf dem lokalen Computer ausführen. Für die Remote-Computer soll die zu verarbeitende Datei an den angegebenen Computer übertragen werden. Wenn my_script beendet, wir wollen foo.out übertragen zurück und wir wollen dann foo.jpeg und foo.out von dem entfernten Computer entfernt:

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

GNU Parallel stellt sicher, von jedem Auftrag der Ausgang nicht, so dass Sie nicht mischen kann die Ausgabe als Eingabe für ein anderes Programm verwenden:

some-command | parallel do-something | postprocess 

die Videos finden Sie weitere Beispiele: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1

+1

Beachten Sie, dass dies sehr nützlich ist, wenn Sie einen 'find' -Befehl verwenden, um eine Dateiliste zu erzeugen, da dies nicht nur das Problem verhindert, wenn innerhalb eines Dateinamens ein Leerzeichen in' for i in ... vorhanden ist; do 'aber find kann auch tun 'find -name \ *. extension1 -oder -name \ *. extension2' welche GNU-Parallele {.} sehr gut verarbeiten kann. –

+0

Plus 1 obwohl die 'Katze' ist, natürlich, [unbrauchbar.] (Http://www.iki.fi/era/unix/award.html) – tripleee

+0

@tripleee Re: Nutzlose Verwendung von Katze. Siehe http://oletange.blogspot.dk/2013/10/useless-use-of-cat.html –

1

Dies ist für die meisten Zwecke gut genug sein könnte, 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 
0

Sie können eine einfache verschachtelte for-Schleife (Ersatz geeignete ganze Zahlen für N und M unten):

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

Dies wird N * M-mal in M ​​Runden, jede Runde Ausführung N Jobs execute do_something parallel zu. Sie können N gleich der Anzahl der CPUs machen, die Sie haben.

3

Funktion für bash:

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

mit:

cat my_commands | parallel -j 4 
+0

Die Verwendung von 'make -j' ist schlau, aber ohne Erklärung und dieses Blob von schreibgeschütztem Awk-Code unterlasse ich das Upvoting. – tripleee

-1

$ domains = "Liste einiger Domäne in Befehle" für foo in some-command tun

eval `some-command for $DOMAINS` & 

    job[$i]=$! 

    i=$((i + 1)) 

done

Ndomains = echo $DOMAINS |wc -w

für $ i (f 1 1 $ Ndomains) tun in echo "für $ warten {job [$ i]}" wait "$ {job [$ i]}" done

in diesem Konzept wird für die Parallelisierung arbeiten. Wichtig ist, dass die letzte Zeile von eval '&' ist, die die Befehle zu Hintergründen bringen wird.

0

Hier ist, wie ich dieses Problem in einem Bash-Skript zu lösen verwaltet:

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

Meine Lösung immer eine bestimmte Anzahl von Prozessen Laufen zu halten, halten Verfolgung von Fehlern und handhaben ubnterruptible/Zombie-Prozesse:

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 
} 

Verbrauch:

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"