Parallelizza un Bash FOR Loop


109

Ho cercato di parallelizzare il seguente script, in particolare ciascuna delle tre istanze del ciclo FOR, usando GNU Parallel, ma non sono stato in grado di farlo. I 4 comandi contenuti all'interno del ciclo FOR vengono eseguiti in serie, ogni ciclo impiega circa 10 minuti.

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

Risposte:


94

Perché non li biforchi (aka. Sfondo)?

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

Nel caso in cui ciò non sia chiaro, la parte significativa è qui:

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

Causare la funzione da eseguire in una shell biforcuta in background. È parallelo.


6
Funzionava come un fascino. Grazie. Un'implementazione così semplice (mi fa sentire così stupido ora!).
Ravnoor S Gill,

8
Nel caso avessi 8 file da eseguire in parallelo ma solo 4 core, potrebbe essere integrato in tale impostazione o richiederebbe un Job Scheduler?
Ravnoor S Gill,

6
Non importa davvero in questo contesto; è normale che il sistema abbia più processi attivi rispetto ai core. Se hai molte attività brevi , idealmente dovresti alimentare una coda gestita da un numero o thread di lavoro <il numero di core. Non so quanto spesso ciò avvenga con lo scripting della shell (nel qual caso, non sarebbero thread, sarebbero processi indipendenti) ma con relativamente pochi compiti sarebbe inutile. Lo scheduler del sistema operativo si prenderà cura di loro.
Riccioli d'oro

17
Potresti anche voler aggiungere un waitcomando alla fine in modo che lo script master non venga chiuso fino a quando non lo fanno tutti i processi in background.
psusi

1
Vorrei anche che sia utile limitare il numero di processi simultanei: i miei processi utilizzano ciascuno il 100% del tempo di un core per circa 25 minuti. Questo è su un server condiviso con 16 core, dove molte persone eseguono lavori. Devo eseguire 23 copie dello script. Se li eseguo tutti contemporaneamente, allora inonderò il server e lo renderò inutile per tutti gli altri per un'ora o due (il carico sale a 30, tutto il resto rallenta). Immagino che potrebbe essere fatto nice, ma poi non so se sarebbe mai finito ..
niente101

150

Attività di esempio

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

Esecuzioni sequenziali

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

Piste parallele

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

Esecuzioni parallele in batch N-process

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

È anche possibile utilizzare FIFO come semafori e utilizzarli per garantire che i nuovi processi vengano generati il ​​più presto possibile e che non vengano eseguiti più di N processi contemporaneamente. Ma richiede più codice.

N processi con un semaforo basato su FIFO:

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 

4
La linea con waitin esso fondamentalmente lascia correre tutti i processi, fino a quando non colpisce il nthprocesso, quindi attende che tutti gli altri finiscano di funzionare, giusto?
naught101,

Se iè zero, chiama attendere. Incremento idopo il test zero.
PSkocik,

2
@ naught101 Sì. waitsenza arg aspetta tutti i bambini. Questo lo rende un po 'dispendioso. L'approccio basato su pipe e semafori offre una maggiore fluidità della concorrenza (lo sto usando in un sistema di build basato su shell personalizzato insieme a -nt/ -otcontrolli da un po 'di tempo)
PSkocik

1
@ BeowulfNode42 Non devi uscire. Lo stato di restituzione dell'attività non danneggerà la coerenza del semaforo fintanto che lo stato (o qualcosa con tale lunghezza) viene riscritto nel quindicesimo dopo che il processo dell'attività termina / si arresta in modo anomalo.
PSkocik,

1
Cordiali saluti, il mkfifo pipe-$$comando richiede un accesso in scrittura appropriato alla directory corrente. Quindi preferisco specificare il percorso completo /tmp/pipe-$$come molto probabilmente ha l'accesso in scrittura disponibile per l'utente corrente piuttosto che fare affidamento su qualunque sia la directory corrente. Sì, sostituire tutte e 3 le occorrenze di pipe-$$.
BeowulfNode42

65
for stuff in things
do
( something
  with
  stuff ) &
done
wait # for all the something with stuff

Se funziona davvero dipende dai tuoi comandi; Non li conosco. La rm *.matsembra un po 'incline a conflitti se si corre in parallelo ...


2
Anche questo funziona perfettamente. Hai ragione, dovrei passare rm *.mata qualcosa di simile rm $run".mat"per farlo funzionare senza che un processo interferisca con l'altro. Grazie .
Ravnoor S Gill,

@RavnoorSGill Benvenuto in Stack Exchange! Se questa risposta ha risolto il tuo problema, contrassegnalo come accettato spuntando il segno di spunta accanto ad esso.
Gilles,

7
+1 per wait, che ho dimenticato.
Riccioli d'oro

5
Se ci sono tonnellate di "cose", questo non avvierà tonnellate di processi? Sarebbe meglio avviare contemporaneamente solo un numero ragionevole di processi, giusto?
David Doria,

1
Consiglio molto utile! Come impostare il numero di thread in questo caso?
Dadong Zhang

30
for stuff in things
do
sem -j+0 ( something
  with
  stuff )
done
sem --wait

Questo utilizzerà semafori, parallelizzando quante più iterazioni quanti sono i core disponibili (-j +0 significa che parallelizzerai i lavori N + 0 , dove N è il numero di core disponibili ).

sem --wait dice di attendere che tutte le iterazioni nel ciclo for abbiano terminato l'esecuzione prima di eseguire le successive righe di codice.

Nota: sarà necessario "parallel" dal progetto parallelo GNU (sudo apt-get install parallel).


1
è possibile superare i 60 anni? il mio genera un errore dicendo che non ci sono abbastanza descrittori di file.
amorevole

Se questo sta generando un errore di sintassi a causa delle parentesi graffe anche per chiunque, dai un'occhiata alla risposta di moritzschaefer.
Nicolai,

10

Un modo davvero semplice che uso spesso:

cat "args" | xargs -P $NUM_PARALLEL command

Questo eseguirà il comando, passando su ciascuna riga del file "args", in parallelo, eseguendo al massimo $ NUM_PARALLEL contemporaneamente.

Puoi anche esaminare l'opzione -I per xargs, se devi sostituire gli argomenti di input in posti diversi.


6

Sembra che i lavori fsl dipendano l'uno dall'altro, quindi i 4 lavori non possono essere eseguiti in parallelo. Le corse, tuttavia, possono essere eseguite in parallelo.

Crea una funzione bash eseguendo una singola corsa ed esegui quella funzione in parallelo:

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

Per saperne di più guarda i video introduttivi: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1 e trascorri un'ora camminando attraverso il tutorial http://www.gnu.org/software/parallel/parallel_tutorial.html Il tuo comando la linea ti amerà per questo.


Se stai usando una shell non bash, dovrai farlo anche export SHELL=/bin/bashprima di correre in parallelo. Altrimenti riceverai un errore come:Unknown command 'myfunc arg'
AndrewHarvey

1
@AndrewHarvey: non è questo lo scopo di Shebang?
naught101,

5

Esecuzione parallela in max N-process simultaneo

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

3

Mi piace molto la risposta di @lev in quanto fornisce il controllo sul numero massimo di processi in modo molto semplice. Tuttavia, come descritto nel manuale , sem non funziona con parentesi.

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

Fa il lavoro.

-j + N Aggiungi N al numero di core della CPU. Esegui fino a questi molti lavori in parallelo. Per lavori ad alta intensità di calcolo -j +0 è utile in quanto eseguirà contemporaneamente lavori con numero di core cpu.

-j -N Sottrae N dal numero di core della CPU. Esegui fino a questi molti lavori in parallelo. Se il numero valutato è inferiore a 1, verrà utilizzato 1. Vedi anche --use-cpus-invece-di-core.


1

Nel mio caso, non posso usare il semaforo (sono in git-bash su Windows), quindi ho trovato un modo generico per dividere l'attività tra N lavoratori, prima che inizino.

Funziona bene se le attività richiedono all'incirca la stessa quantità di tempo. Lo svantaggio è che, se uno dei lavoratori impiega molto tempo a svolgere la propria parte del lavoro, gli altri che hanno già terminato non saranno d'aiuto.

Dividere il lavoro tra N lavoratori (1 per nucleo)

# 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

0

Ho avuto problemi con @PSkocikla soluzione. Il mio sistema non ha GNU Parallel disponibile come pacchetto e semha generato un'eccezione quando l'ho creato ed eseguito manualmente. Ho quindi provato anche l'esempio del semaforo FIFO che ha anche gettato alcuni altri errori riguardanti la comunicazione.

@eyeApps mi ha suggerito xargs ma non sapevo come farlo funzionare con il mio caso d'uso complesso (gli esempi sarebbero i benvenuti).

Ecco la mia soluzione per lavori paralleli che elaborano fino a Nlavori contemporaneamente configurati da _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
}

Esempio di utilizzo:

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