Parallelizza lo script Bash con il numero massimo di processi


86

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 bel processore a 4 core. Mi piacerebbe essere in grado di eseguire fino a 4 do-somethingalla volta.

L'approccio ingenuo sembra essere:

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

Questo funzionerà tutti do-something in una volta, ma ci sono un paio di aspetti negativi, principalmente che fare qualcosa può anche avere un I / O significativo che eseguire 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 do-somethingi messaggi sono finiti.

Come scriveresti questo ciclo in modo che ci siano sempre X in do-somethingesecuzione contemporaneamente?


2
Come sidenode, ho sognato di aggiungere l'opzione -j di make alla bash per primitive. Non funzionerebbe sempre, ma per alcuni semplici casi in cui sai che il corpo del ciclo farà qualcosa di unico per ogni iterazione, sarebbe abbastanza chiaro dire solo "per -j 4 ...".
rilassarsi

1
Riferimento incrociato a stackoverflow.com/questions/1537956/… per una soluzione bash che mitiga i problemi di prestazioni e consente gruppi di sottoprocessi, tenuti separati.
paxdiablo

1
Consiglierei la mia soluzione stackoverflow.com/a/28965927/340581
Tuttle

Risposte:


62

A seconda di cosa vuoi fare, anche xargs può aiutare (qui: convertire 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.

9
Questo metodo, secondo me, è la soluzione più elegante. Tranne che, dato che sono paranoico, mi piace sempre usare find [...] -print0e xargs -0.
amphetamachine

7
cpus=$(getconf _NPROCESSORS_ONLN)
mr.spuratic

1
Dal manuale, perché non utilizzare --max-procs=0per ottenere il maggior numero di processi possibile?
EverythingRightPlace

@EverythingRightPlace, la domanda non richiede esplicitamente più processi dei processori disponibili. --max-procs=0è più simile al tentativo dell'interrogante (avvia tanti processi quanti sono gli argomenti).
Toby Speight

39

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

some-command | parallel do-something

GNU Parallel supporta anche l'esecuzione di lavori su computer remoti. Verrà eseguito uno per core 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 che my_script venga eseguito. I file hanno estensione (forse .jpeg). Vogliamo che l'output di my_script sia messo accanto ai file in basename.out (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 e trasferito sul computer specificato. Quando my_script finisce, vogliamo ritrasferire foo.out e quindi vogliamo che foo.jpeg e foo.out vengano 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


1
Nota che questo è davvero utile quando si usa un findcomando per generare un elenco di file, perché non solo previene il problema quando c'è uno spazio all'interno di un nome di file che si trova in, for i in ...; doma find può anche fare find -name \*.extension1 -or -name \*.extension2quale parallelo GNU {.} Può gestire molto bene.
Leo Izen

Plus 1 sebbene catsia, ovviamente, inutile.
tripleee

@tripleee Re: Uso inutile del gatto. Vedi oletange.blogspot.dk/2013/10/useless-use-of-cat.html
Ole Tange

Oh sei tu! Per inciso, potresti aggiornare il link su quel blog? La posizione di partmaps.org è purtroppo morta, ma il redirector Iki dovrebbe continuare a funzionare.
tripleee

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

parallelizza arg1 arg2 "5 argomenti al terzo lavoro" arg4 ...

10
Renditi conto che qui sono in corso alcune gravi sottoquotazioni, quindi qualsiasi lavoro che richiede spazi negli argomenti fallirà gravemente; inoltre, questo script manterrà viva la tua CPU mentre attende che alcuni lavori finiscano se sono richiesti più lavori di quelli consentiti da maxjobs.
lhunath

1
Notare inoltre che questo presume che lo script non stia facendo nient'altro a che fare con i lavori; se lo sei, conterà anche quelli per maxjobs.
lhunath

1
Potresti voler usare "jobs -pr" per limitare l'esecuzione dei lavori.
amphetamachine

1
Aggiunto un comando sleep per evitare che il ciclo while si ripeta senza interruzioni, mentre attende che finisca di eseguire comandi già in esecuzione. Altrimenti, questo ciclo occuperebbe essenzialmente uno dei core della CPU. Questo affronta anche la preoccupazione di @lhunath.
euforia83

12

Ecco una soluzione alternativa che può essere inserita in .bashrc e utilizzata per un liner quotidiano:

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

Per usarlo, tutto quello che bisogna fare è mettere &dopo i lavori e una chiamata in attesa, il parametro fornisce il numero di processi paralleli:

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

Sarebbe più bello da usare waitinvece di essere impegnati ad aspettare l'output di jobs -p, ma non sembra esserci una soluzione ovvia per aspettare che uno qualsiasi dei lavori dati sia finito invece di tutti.


11

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

Oppure puoi usare wait(" man wait"): lancia diversi processi figli, chiama 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, assegnare il risultato a una variabile. Dopo waitaver controllato cosa contiene la variabile.


1
Grazie per questo, anche se il codice non è finito, mi ha dato la risposta a un problema che sto riscontrando al lavoro.
gerikson

l'unico guaio è che se uccidi lo script in primo piano (quello con il loop) i lavori in esecuzione non verranno uccisi insieme
Girardi

8

Forse provare un'utilità di parallelizzazione invece di riscrivere il ciclo? Sono un grande fan di xjobs. Uso sempre xjobs per copiare file in massa sulla nostra rete, di solito quando imposto un nuovo server di database. http://www.maier-komor.de/xjobs.html


7

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

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

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

tutti: $ (OUTPUT)

e poi corri

make -j <NUMBER>

per eseguire al massimo NUMERO comandi in parallelo.


6

Anche se farlo bene bashè probabilmente impossibile, puoi fare un semi-destro abbastanza facilmente. bstarkha dato una buona approssimazione del diritto ma il suo ha i seguenti difetti:

  • Divisione di parole: non è possibile passare ad esso alcun lavoro che utilizza uno dei seguenti caratteri nei loro argomenti: spazi, tabulazioni, nuove righe, stelle, punti interrogativi. Se lo fai, le cose si romperanno, forse inaspettatamente.
  • Si basa sul resto del tuo script per non creare alcun background. Se lo fai, o in seguito aggiungi qualcosa allo script che viene inviato in background perché hai dimenticato di non essere autorizzato a utilizzare lavori in background a causa del suo frammento, le cose si interromperanno.

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 ogni lavoro al termine in modo da poter avvisare l'utente se un lavoro fallisce o impostare un codice di uscita in scheduleAllbase alla quantità di lavori non riusciti, o qualcosa del genere.

Il problema con questo codice è proprio questo:

  • Pianifica quattro (in questo caso) lavori alla volta e quindi attende che tutti e quattro finiscano. Alcuni potrebbero essere eseguiti prima di altri, il che causerà l'attesa del batch successivo di quattro lavori fino al completamento del batch più lungo del batch precedente.

Una soluzione che si occupi di questo ultimo problema dovrebbe essere utilizzata kill -0per verificare se uno dei processi è scomparso invece di waite pianificare il lavoro successivo. Tuttavia, ciò introduce un piccolo nuovo problema: hai una condizione di gara tra la fine di un lavoro e il kill -0controllo se è terminato. Se il lavoro è terminato e un altro processo sul sistema si avvia contemporaneamente, prendendo un PID casuale che è quello del lavoro appena finito, kill -0non si accorgerà che il tuo lavoro è finito e le cose si interromperanno di nuovo.

Una soluzione perfetta non è possibile in bash.


3

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

utilizzando:

cat my_commands | parallel -j 4

L'uso di make -jè intelligente, ma senza alcuna spiegazione e con quel blob di codice Awk di sola scrittura, evito di votare.
tripleee

2

Il progetto su cui lavoro utilizza il comando wait per controllare i processi della shell parallela (ksh in realtà). Per rispondere alle tue preoccupazioni sull'IO, su un sistema operativo moderno, è possibile che l'esecuzione parallela aumenti effettivamente l'efficienza. Se tutti i processi stanno leggendo gli stessi blocchi sul 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 sistema operativo in memoria. Ovviamente, la lettura dalla memoria è di diversi ordini di grandezza più veloce della lettura dal disco. Inoltre, il vantaggio non richiede modifiche alla codifica.


1

Questo potrebbe essere sufficiente 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

1

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

1

Davvero tardi per la festa qui, ma ecco un'altra soluzione.

Molte soluzioni non gestiscono spazi / caratteri speciali nei comandi, non mantengono sempre in esecuzione N lavori, mangiano cpu in cicli occupati o si affidano a dipendenze esterne (es. GNU parallel).

Con l' ispirazione per la gestione dei processi morti / zombi , ecco una pura soluzione bash:

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
}

E utilizzo di esempio:

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[@]}"

Il risultato:

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

Per la gestione dell'output per processo $$potrebbe essere utilizzata per accedere a un file, ad esempio:

function job_done {
    cat "$1.log"
}

cmds=( \
    "echo 1 \$\$ >\$\$.log" \
    "echo 2 \$\$ >\$\$.log" \
)

run_parallel_jobs 2 "job_done" "${cmds[@]}"

Produzione:

1 56871
2 56872

0

È possibile utilizzare un semplice ciclo for annidato (sostituire gli 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_something N * M volte in M ​​round, ogni round eseguirà N job in parallelo. Puoi rendere N uguale al numero di CPU che hai.


0

La mia soluzione per mantenere sempre in esecuzione un determinato numero di processi, tenere traccia degli errori e gestire 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
}

Utilizzo:

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"

-1

$ DOMAINS = "elenco di alcuni comandi di dominio" per pippo in some-command do

eval `some-command for $DOMAINS` &

    job[$i]=$!

    i=$(( i + 1))

fatto

Ndomains =echo $DOMAINS |wc -w

for i in $ (seq 1 1 $ Ndomains) do echo "aspetta $ {job [$ i]}" aspetta "$ {job [$ i]}" fatto

in questo concetto funzionerà per il parallelizzare. la cosa importante è che l'ultima riga di eval è '&' che metterà i comandi in background.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.