Parallelizza lo script di Bash con il numero massimo di processi

Diciamo che ho un ciclo in Bash:

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

do-something è legato alla cpu e ho un buon processore a 4 core. Mi piacerebbe essere in grado di eseguire fino a 4 do-something in una volta.

L’approccio ingenuo sembra essere:

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

Questo eseguirà tutti i do-something allo stesso tempo, ma ci sono un paio di aspetti negativi, principalmente questo: qualcosa potrebbe anche avere un I / O significativo che eseguendo tutto in una volta potrebbe rallentare un po ‘. L’altro problema è che questo blocco di codice ritorna immediatamente, quindi non c’è modo di fare altro lavoro quando tutti i do-something sono finiti.

Come scriveresti questo ciclo quindi ci sono sempre X do-something esecuzione contemporaneamente?

A seconda di cosa vuoi fare, anche xargs può aiutarti (qui: convertire i documenti con pdf2ps):

 cpus=$( ls -d /sys/devices/system/cpu/cpu[[:digit:]]* | wc -w ) find . -name \*.pdf | xargs --max-args=1 --max-procs=$cpus pdf2ps 

Dai documenti:

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

Con GNU Parallel http://www.gnu.org/software/parallel/ puoi scrivere:

 some-command | parallel do-something 

GNU Parallel supporta anche i lavori in esecuzione su computer remoti. Questo eseguirà uno per core della CPU sui computer remoti, anche se hanno un numero diverso di core:

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

Un esempio più avanzato: qui elenchiamo i file su cui vogliamo far girare my_script. I file hanno estensione (forse .jpeg). Vogliamo che l’output di my_script sia messo accanto ai file in basename.out (ad es. Foo.jpeg -> foo.out). Vogliamo eseguire my_script una volta per ogni core del computer e vogliamo eseguirlo anche sul computer locale. Per i computer remoti vogliamo che il file venga elaborato trasferito sul computer specificato. Quando termina my_script, vogliamo che foo.out venga trasferito e quindi vogliamo che foo.jpeg e foo.out siano rimossi dal computer remoto:

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

GNU Parallel si assicura che l’output di ogni lavoro non si mischi, quindi puoi usare l’output come input per un altro programma:

 some-command | parallel do-something | postprocess 

Guarda i video per altri esempi: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1

 MAXJOBS = 4
 parallelize () {
         while [$ # -gt 0];  fare
                 jobcnt = (`jobs -p`)
                 se [$ {# jobcnt [@]} -lt $ maxjobs];  poi
                         fare qualcosa $ 1 e
                         cambio  
                 altro
                         dormire 1
                 fi
         fatto
         aspettare
 }

 parallelizzare arg1 arg2 "5 args al terzo lavoro" arg4 ...

Invece di una semplice bash, usa un Makefile, quindi specifica il numero di lavori simultanei con make -jX dove X è il numero di lavori da eseguire contemporaneamente.

Oppure puoi usare wait (” man wait “): avvia diversi processi figli, richiama l’ wait – uscirà quando i processi figli finiranno.

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

Se è necessario memorizzare il risultato del lavoro, quindi assegnare il risultato a una variabile. Dopo aver wait , basta controllare cosa contiene la variabile.

Forse provare un programma di parallelizzazione invece di riscrivere il ciclo? Sono un grande fan di xjobs. Io uso xjobs tutto il tempo per copiare in massa i file sulla nostra rete, di solito quando si imposta un nuovo server di database. http://www.maier-komor.de/xjobs.html

Ecco una soluzione alternativa che può essere inserita in .bashrc e utilizzata per una fodera quotidiana:

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

Per usarlo, tutto ciò che si deve fare è mettere & dopo i lavori e una chiamata pwait, il parametro indica il numero di processi paralleli:

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

Sarebbe preferibile utilizzare l’ wait anziché l’attesa sull’output dei jobs -p , ma non sembra esserci una soluzione ovvia per attendere che uno qualsiasi dei lavori dati sia finito invece di tutti.

Se farlo correttamente in bash è probabilmente imansible, puoi fare una semi destra abbastanza facilmente. bstark dato una giusta approssimazione di destra, ma ha i seguenti difetti:

  • Suddivisione di parole: non è ansible passare a nessuno dei lavori che utilizzano uno dei seguenti caratteri nei loro argomenti: spazi, tabulazioni, nuove righe, stelle, punti interrogativi. Se lo fai, le cose si romperanno, forse in modo imprevisto.
  • Si basa sul resto del tuo script per non creare alcun background. Se lo fai, o più tardi aggiungi qualcosa allo script che viene inviato in background perché hai dimenticato che non ti è stato permesso di utilizzare lavori in background a causa del suo frammento, le cose si romperanno.

Un’altra approssimazione che non ha questi difetti è la seguente:

 scheduleAll() { local job i=0 max=4 pids=() for job; do (( ++i % max == 0 )) && { wait "${pids[@]}" pids=() } bash -c "$job" & pids+=("$!") done wait "${pids[@]}" } 

Si noti che questo è facilmente adattabile per controllare anche il codice di uscita di ciascun lavoro al termine, in modo da poter avvisare l’utente se un lavoro non riesce o impostare un codice di uscita per scheduleAll base alla quantità di lavori non riusciti, o qualcosa del genere.

Il problema con questo codice è proprio questo:

  • Pianifica quattro lavori (in questo caso) alla volta e attende che tutti e quattro finiscano. Alcuni potrebbero essere eseguiti prima di altri, il che farà sì che il successivo lotto di quattro lavori attenda fino al più lungo del batch precedente.

Una soluzione che si occupa di questo ultimo problema dovrebbe utilizzare kill -0 per verificare se uno qualsiasi dei processi è scomparso anziché wait e pianificare il lavoro successivo. Tuttavia, questo introduce un piccolo nuovo problema: si ha una condizione di competizione tra una fine del lavoro, e la kill -0 controlla se è finita. Se il lavoro termina e un altro processo sul sistema si avvia allo stesso tempo, prendendo un PID casuale che sembra essere quello del lavoro appena terminato, kill -0 non noterà il completamento del lavoro e le cose si interromperanno di nuovo .

Una soluzione perfetta non è ansible in bash .

Se hai familiarità con il comando make , la maggior parte delle volte puoi esprimere l’elenco di comandi che vuoi eseguire come un makefile. Ad esempio, se è necessario eseguire $ SOME_COMMAND su file * .input ognuno dei quali produce * .output, è ansible utilizzare il makefile

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

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

 tutto: $ (OUTPUT)

e poi corri

 fai -j 

per eseguire al massimo NUMBER comandi in parallelo.

funzione per 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 } 

utilizzando:

 cat my_commands | parallel -j 4 

Il progetto su cui lavoro utilizza il comando wait per controllare i processi della shell parallela (in realtà ksh). Per affrontare le tue preoccupazioni su IO, su un sistema operativo moderno, è ansible che l’esecuzione parallela aumenti effettivamente l’efficienza. Se tutti i processi stanno leggendo gli stessi blocchi su disco, solo il primo processo dovrà colpire l’hardware fisico. Gli altri processi saranno spesso in grado di recuperare il blocco dalla cache del disco del SO in memoria. Ovviamente, la lettura dalla memoria è di diversi ordini di grandezza più veloce della lettura dal disco. Inoltre, il vantaggio non richiede modifiche di codifica.

Questo potrebbe essere abbastanza buono per la maggior parte degli scopi, ma non è ottimale.

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

È ansible utilizzare un ciclo semplice annidato (sostituendo interi appropriati per N e M di seguito):

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

Questo eseguirà do_qualcosa N * volte M in round M, ogni round eseguendo N lavori in parallelo. Puoi rendere N uguale al numero di CPU che hai.

Ecco come sono riuscito a risolvere questo problema in uno script bash:

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

La mia soluzione per mantenere sempre un certo numero di processi in esecuzione, tenere traccia degli errori e gestire i processi ubnterruptible / zombie:

 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 } 

Uso:

 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" 

$ DOMAIN = “elenco di alcuni domini nei comandi” per foo in some-command do

 eval `some-command for $DOMAINS` & job[$i]=$! i=$(( i + 1)) 

fatto

Ndomains = echo $DOMAINS |wc -w DOMAINS echo $DOMAINS |wc -w

per i in $ (seq 1 1 $ Ndomains) fai echo “wait for $ {job [$ i]}” wait “$ {job [$ i]}” fatto

in questo concetto funzionerà per il parallelizzare. cosa importante è l’ultima riga di valutazione è ‘&’ che metterà i comandi agli sfondi.