Domanda

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 brillante.Mi piacerebbe poter correre fino a 4 do-somethingè subito.

L’approccio ingenuo sembra essere:

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

Questo funzionerà Tutto do-somethings in una sola volta, ma ci sono un paio di aspetti negativi, principalmente che fare qualcosa può anche avere alcuni I/O significativi che l'esecuzione Tutto subito potrebbe rallentare un po'.L'altro problema è che questo blocco di codice ritorna immediatamente, quindi non c'è modo di fare altro lavoro quando tutto il file do-somethingsono finiti.

Come scriveresti questo ciclo in modo che ci siano sempre X do-somethingsta correndo subito?

È stato utile?

Soluzione

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

Altri suggerimenti

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

some-command | parallel do-something

GNU Parallel supporta anche l'esecuzione di lavori su computer remoti.Ne 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 venga inserito 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 al computer specificato.Quando my_script termina, vogliamo che foo.out venga ritrasferito e poi 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 ciascun lavoro non si mescoli, quindi puoi utilizzare l'output come input per un altro programma:

some-command | parallel do-something | postprocess

Guarda i video per ulteriori esempi: 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 ...

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"):avviare diversi processi figli, chiamare wait - uscirà al termine dei processi figli.

maxjobs = 10

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

job ( ){
...
}

Se devi memorizzare il risultato del lavoro, assegna il risultato a una variabile.Dopo wait devi solo controllare cosa contiene la variabile.

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

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

Per utilizzarlo basta mettere & dopo i lavori e una chiamata pwait, il parametro fornisce il numero di processi paralleli:

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

Sarebbe più bello da usare wait invece di essere occupato ad aspettare l'output di jobs -p, ma non sembra esserci una soluzione ovvia: aspettare che venga completato uno qualsiasi dei lavori anziché tutti.

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

Mentre lo faccio proprio dentro bash è probabilmente impossibile, puoi fare un semi-destro abbastanza facilmente. bstark ha dato una buona approssimazione del giusto ma ha i seguenti difetti:

  • Divisione delle parole:Non puoi passargli alcun lavoro che utilizzi uno dei seguenti caratteri nei loro argomenti:spazi, tabulazioni, ritorni a capo, stelle, punti interrogativi.Se lo fai, le cose si romperanno, forse inaspettatamente.
  • Si basa sul resto dello script per non creare alcun background.Se lo fai, o in seguito aggiungi qualcosa allo script che viene inviato in background perché hai dimenticato che non ti era consentito utilizzare lavori in background a causa del suo snippet, le cose si interrompono.

Un'altra approssimazione che non presenta 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[@]}"
}

Tieni presente 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 fallisce o impostare un codice di uscita per scheduleAll in base alla quantità di lavori falliti o qualcosa del genere.

Il problema con questo codice è proprio questo:

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

Sarebbe necessario utilizzare una soluzione che si occupi di quest'ultimo problema kill -0 per verificare se qualcuno dei processi è scomparso invece del wait e programmare il lavoro successivo.Tuttavia, ciò introduce un piccolo nuovo problema:hai una condizione di competizione tra la fine del lavoro e il kill -0 controllando se è finita.Se il lavoro è terminato e contemporaneamente si avvia un altro processo sul sistema, prendendo un PID casuale che sembra essere quello del lavoro appena terminato, il kill -0 non noterai che il tuo lavoro è finito e le cose si romperanno di nuovo.

Una soluzione perfetta non è possibile in bash.

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

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

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

all: $(OUTPUT)

e poi semplicemente corri

make -j<NUMBER>

per eseguire al massimo NUMBER comandi in parallelo.

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

Il progetto su cui lavoro utilizza il file Aspettare comando 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 leggono 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 è diversi ordini di grandezza più veloce della lettura dal disco.Inoltre, il vantaggio non richiede modifiche alla codifica.

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

Veramente tardi alla festa qui, ma ecco un'altra soluzione.

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

Con ispirazione per la gestione del processo morti/zombi, ecco una soluzione bash pura:

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 del campione:

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

Puoi utilizzare un semplice ciclo for nidificato (sostituisci N e M con numeri interi appropriati di seguito):

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

Questo eseguirà do_qualcosa N*M volte in M ​​round, 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 in esecuzione un determinato numero di processi, tenere traccia degli errori e gestire processi non interrompibili/zombi:

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"

$ Domains = "Elenco di alcuni domini nei comandi" per foo in some-commandFare

eval `some-command for $DOMAINS` &

    job[$i]=$!

    i=$(( i + 1))

Fatto

Ndomini=echo $DOMAINS |wc -w

Per i in $ (seq 1 1 $ ndomains) fai eco "Aspetta $ {job [$ i]}" wait "$ {job [$ i]}" fatto

in questo concetto funzionerà per il parallelismo.La cosa importante è che l'ultima riga di Eval è '&' che metterà i comandi agli sfondi.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top