Paralléliser une boucle Bash FOR

109

J'ai essayé de paralléliser le script suivant, en particulier chacune des trois instances de boucle FOR, en utilisant GNU Parallel mais je n'ai pas pu le faire. Les 4 commandes contenues dans la boucle FOR sont exécutées 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
Ravnoor S Gill
la source

Réponses:

94

Pourquoi ne pas simplement les fourrer (aka. Fond)?

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

Au cas où ce ne soit pas clair, l'essentiel est ici:

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

Causer la fonction à être exécutée dans un shell forké en arrière-plan. C'est parallèle.

boucle d'or
la source
6
Cela a fonctionné comme un charme. Je vous remercie. Une telle mise en œuvre simple (ça me fait me sentir si stupide maintenant!).
Ravnoor S Gill
8
Au cas où j’aurais 8 fichiers à exécuter en parallèle mais seulement 4 cœurs, cela pourrait-il être intégré dans un tel paramètre ou faudrait-il un planificateur de tâches?
Ravnoor S Gill
6
Cela n'a pas vraiment d'importance dans ce contexte; il est normal que le système ait plus de processus actifs que de cœurs. Si vous avez plusieurs tâches courtes , l'idéal serait de nourrir une file d'attente desservie par un nombre de threads de travail inférieur au nombre de cœurs. Je ne sais pas combien de fois cela est vraiment fait avec les scripts shell (dans ce cas, ce ne seraient pas des threads, ce seraient des processus indépendants), mais avec relativement peu de longues tâches, cela serait inutile. Le planificateur de système d'exploitation s'en occupera.
goldilocks
17
Vous pouvez également vouloir ajouter une waitcommande à la fin pour que le script maître ne se ferme pas tant que tous les travaux en arrière-plan ne sont pas terminés.
Psusi
1
Je souhaiterais également qu'il soit utile de limiter le nombre de processus simultanés: mes processus utilisent chacun 100% du temps d'un cœur pendant environ 25 minutes. Ceci est sur un serveur partagé avec 16 cœurs, où beaucoup de personnes exécutent des travaux. Je dois exécuter 23 copies du script. Si je les exécute tous en même temps, je submerge le serveur et le rend inutilisable pour tous les autres pendant une heure ou deux (la charge monte à 30, tout le reste ralentit considérablement). Je suppose que cela pourrait être fait avec nice, mais alors je ne sais pas si cela finirait jamais ..
naught101
152

Exemple de tâche

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

Pistes séquentielles

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

Courses parallèles

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

Les exécutions parallèles dans des lots de N-processus

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 s'assurer que les nouveaux processus sont générés le plus rapidement possible et que pas plus de N processus ne s'exécutent en même temps. Mais cela nécessite plus de code.

N processus avec un sémaphore basé sur 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 
PSkocik
la source
4
La ligne waitqui la contient permet en principe que tous les processus s’exécutent, jusqu’à ce qu’ils atteignent le nthprocessus, puis attend que tous les autres s’achèvent, est-ce exact?
naught101
Si iest égal à zéro, appelez attendre. Incrément iaprès le test zéro.
PSkocik
2
@ naught101 Oui. waitw / no arg attend tous les enfants. Cela fait un peu de gaspillage. L'approche basée sur les sémaphores de pipe vous donne une concurrence plus fluide (je l'utilise dans un système de construction basé sur un shell personnalisé avec -nt/ -otchecks avec succès depuis un moment maintenant)
PSkocik
1
@ BeowulfNode42 Vous n'êtes pas obligé de quitter. Le statut de retour de la tâche n'affectera pas la cohérence du sémaphore tant que le statut (ou quelque chose avec cette longueur) est réécrit sur le fifo après que le processus de la tâche se soit arrêté / arrêté.
PSkocik
1
Pour votre information, la mkfifo pipe-$$commande nécessite un accès en écriture approprié au répertoire actuel. Donc, je préfère spécifier le chemin complet tel /tmp/pipe-$$qu'il a très probablement un accès en écriture disponible pour l'utilisateur actuel plutôt que de compter sur le répertoire actuel. Oui, remplacez les 3 occurrences de pipe-$$.
BeowulfNode42 le
65
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 *.matsemble un peu sujet aux conflits si elle s'exécute en parallèle ...

Frostschutz
la source
2
Cela fonctionne parfaitement aussi bien. Vous avez raison, il faudrait que je passe rm *.matà quelque chose comme rm $run".mat"pour le faire fonctionner sans que l'un des processus interfère avec l'autre. Je vous remercie .
Ravnoor S Gill
@RavnoorSGill Bienvenue à Stack Exchange! Si cette réponse résout votre problème, veuillez le marquer comme accepté en cochant la case correspondante.
Gilles
7
+1 pour wait, que j'ai oublié.
goldilocks
5
S'il y a des tonnes de «choses», cela ne démarrera-t-il pas des tonnes de processus? Il serait préférable de ne commencer qu'un nombre raisonnable de processus simultanément, n'est-ce pas?
David Doria
1
Conseil très utile! Comment configurer le nombre de threads dans ce cas?
Dadong Zhang
30
for stuff in things
do
sem -j+0 ( something
  with
  stuff )
done
sem --wait

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

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

Note: vous aurez besoin du "parallèle" du projet parallèle GNU (sudo apt-get install parallel).

lev
la source
1
est-il possible d'aller au-delà de 60 ans? le mien jette une erreur en disant pas assez de descripteurs de fichiers.
Chovy
Si cela jette une erreur de syntaxe à cause des accolades de tout le monde, jetez un œil à la réponse de moritzschaefer.
Nicolai
10

Un moyen très simple que j'utilise souvent:

cat "args" | xargs -P $NUM_PARALLEL command

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

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

eyeApps LLC
la source
6

Il semble que les travaux fsl dépendent les uns des autres, de sorte que les 4 travaux ne peuvent pas être exécutés en parallèle. Les pistes, cependant, peuvent être exécutées en parallèle.

Créez une fonction bash exécutant une seule exécution et exécutez-la 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 commande La ligne vous aimera pour cela.

Ole Tange
la source
Si vous utilisez un shell non-bash, vous devrez également export SHELL=/bin/bashavant d'exécuter en parallèle. Sinon, vous obtiendrez une erreur du type:Unknown command 'myfunc arg'
AndrewHarvey le
1
@ AndrewHarvey: n'est-ce pas le but du shebang?
naught101
5

Exécution parallèle dans le nombre maximum de processus simultanés

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

J'aime beaucoup la réponse de @lev car elle permet de contrôler très simplement le maximum de processus. Cependant, comme décrit dans le manuel , sem ne fonctionne pas avec des 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 la CPU. Courir à autant de travaux en parallèle. Pour les travaux intensifs en calcul, l'option -j +0 est utile car elle exécute simultanément des travaux à nombre de cpu-cœurs.

-j -N Soustrayez N du nombre de cœurs de la CPU. Courir à autant de travaux en parallèle. Si le nombre évalué est inférieur à 1, 1 sera utilisé. Voir aussi --use-cpus-lieu-de-cœurs.

moritzschaefer
la source
1

Dans mon cas, je ne peux pas utiliser le sémaphore (je suis dans git-bash sous Windows), alors je suis arrivé avec un moyen générique de diviser 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 longtemps à faire son travail, les autres qui ont déjà terminé ne l'aideront pas.

Division du travail entre N travailleurs (1 par noyau)

# 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
geekley
la source
0

J'ai eu du mal avec @PSkocikla solution de. GNU Parallel n’est pas disponible dans mon système en tant que package et a semgénéré une exception lorsque je l’ai construit et exécuté manuellement. J'ai ensuite essayé l'exemple du sémaphore FIFO, qui a également généré d'autres erreurs en matière de communication.

@eyeApps xargs suggéré 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'à des Ntravaux à 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
Zhro
la source