web-dev-qa-db-fra.com

Paralléliser une boucle Bash FOR

J'ai essayé de paralléliser le script suivant, en particulier chacune des trois instances de boucle FOR, en utilisant GNU Parallèle mais je n'ai pas pu. Les 4 commandes contenues dans la boucle FOR s'exécutent en série , chaque boucle prenant environ 10 minutes.

#!/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
128
Ravnoor S Gill

Pourquoi ne les fourches-tu pas (aka. Background)?

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

Dans le cas où ce n'est pas clair, la partie importante est ici:

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

Causer la fonction à exécuter dans un Shell fourchu en arrière-plan. C'est parallèle.

110
goldilocks

Exemple de tâche

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

Exécutions séquentielles

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

Exécutions parallèles

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

Exécutions parallèles par lots de processus N

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

Il est également possible d'utiliser des FIFO comme sémaphores et de les utiliser pour garantir que les nouveaux processus sont générés dès que possible et que pas plus de N processus s'exécutent en même temps. Mais cela nécessite plus de code.

N processus avec un sémaphore basé sur FIFO:

# initialize a semaphore with a given number of tokens
open_sem(){
    mkfifo pipe-$$
    exec 3<>pipe-$$
    rm pipe-$$
    local i=$1
    for((;i>0;i--)); do
        printf %s 000 >&3
    done
}

# run the given command asynchronously and pop/Push tokens
run_with_lock(){
    local x
    # this read waits until there is something to read
    read -u 3 -n 3 x && ((0==x)) || exit $x
    (
     ( "$@"; )
    # Push the return code of the command to the semaphore
    printf '%.3d' $? >&3
    )&
}

N=4
open_sem $N
for thing in {a..g}; do
    run_with_lock task $thing
done 

Explication:

Nous utilisons le descripteur de fichier 3 comme sémaphore en poussant (= printf) et en faisant apparaître (= read) des jetons ('000'). En poussant le code retour des tâches exécutées, nous pouvons abandonner en cas de problème.

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

Que cela fonctionne réellement dépend de vos commandes; Je ne les connais pas. Le rm *.mat semble un peu sujet aux conflits s'il s'exécute en parallèle ...

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

Cela utilisera des sémaphores, parallélisant autant d'itérations que le nombre de cœurs disponibles (-j +0 signifie que vous paralléliserez N + 0 travaux , où N est le nombre de cœurs disponibles ).

sem --wait indique d'attendre que toutes les itérations de la boucle for aient terminé leur exécution avant d'exécuter les lignes de code successives.

Remarque: vous aurez besoin de "parallèle" à partir du projet parallèle GN (Sudo apt-get install parallel).

30
lev

Un moyen vraiment simple que j'utilise souvent:

cat "args" | xargs -P $NUM_PARALLEL command

Cela exécutera la commande, en passant dans chaque ligne du fichier "args", en parallèle, en exécutant au plus $ NUM_PARALLEL en même temps.

Vous pouvez également rechercher l'option -I pour xargs, si vous devez remplacer les arguments d'entrée à différents endroits.

11
eyeApps LLC

Il semble que les tâches fsl dépendent les unes des autres, donc les 4 tâches ne peuvent pas être exécutées en parallèle. Cependant, les analyses peuvent être exécutées en parallèle.

Créez une fonction bash exécutant une seule exécution et exécutez cette fonction en parallèle:

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

Pour en savoir plus, regardez les vidéos d'introduction: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1 et passez une heure à parcourir le didacticiel http: //www.gnu. org/software/parallel/parallel_tutorial.html Votre ligne de commande vous aimera pour cela.

7
Ole Tange

Exécution parallèle en simultané max N-processus

#!/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"
7
Tomasz Hławiczka

J'aime vraiment la réponse de @lev car elle permet de contrôler le nombre maximum de processus d'une manière très simple. Cependant, comme décrit dans le manuel , sem ne fonctionne pas avec les crochets.

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

Fait le travail.

-j + N Ajoutez N au nombre de cœurs de processeur. Exécutez en parallèle ce nombre d'emplois. Pour les travaux intensifs en calcul, -j +0 est utile car il exécutera simultanément des travaux nombre de cpu-cores.

-j -N Soustraire N du nombre de cœurs de processeur. Exécutez en parallèle ce nombre d'emplois. Si le nombre évalué est inférieur à 1, 1 sera utilisé. Voir aussi --use-cpus-lieu-of-cores.

5
moritzschaefer

Dans mon cas, je ne peux pas utiliser de sémaphore (je suis en git-bash sur Windows), j'ai donc trouvé un moyen générique de répartir la tâche entre N travailleurs, avant qu'ils ne commencent.

Cela fonctionne bien si les tâches prennent à peu près le même temps. L'inconvénient est que, si l'un des travailleurs met beaucoup de temps à faire sa part de travail, les autres qui ont déjà terminé n'aideront pas.

Répartition de l'emploi entre N travailleurs (1 par cœur)

# 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
2
geekley

J'ai eu des problèmes avec la solution de @PSkocik. Mon système n'a pas GNU Parallel disponible en tant que package et sem a levé une exception lorsque je l'ai construit et exécuté manuellement. J'ai ensuite essayé l'exemple de sémaphore FIFO qui a également jeté quelques autres erreurs concernant la communication.

@eyeApps A suggéré xargs mais je ne savais pas comment le faire fonctionner avec mon cas d'utilisation complexe (des exemples seraient les bienvenus).

Voici ma solution pour les travaux parallèles qui traitent jusqu'à N travaux à la fois comme configuré par _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
}

Exemple d'utilisation:

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