Comment exécutez-vous plusieurs programmes en parallèle à partir d'un script bash?

245

J'essaie d'écrire un fichier .sh qui exécute plusieurs programmes simultanément

J'ai essayé

prog1 
prog2

Mais cela exécute prog1 puis attend la fin de prog1 puis démarre prog2 ...

Alors, comment puis-je les exécuter en parallèle?

Betamoo
la source

Réponses:

216
prog1 &
prog2 &
psmears
la source
49
N'oubliez pas le wait! Oui, dans bash, vous pouvez attendre les processus enfants du script.
Dummy00001
5
Une autre option consiste à nohupempêcher le programme d'être tué lorsque le shell raccroche.
Philipp
@liang: Oui, cela fonctionnera également avec trois programmes ou plus.
psmears
302

Que diriez-vous:

prog1 & prog2 && fg

Cette volonté:

  1. Commencez prog1.
  2. Envoyez-le en arrière-plan, mais continuez d'imprimer sa sortie.
  3. Commencez prog2et gardez-le au premier plan pour pouvoir le fermer ctrl-c.
  4. Lorsque vous fermez prog2, vous revenez à prog1de » l'avant - plan , de sorte que vous pouvez refermer avec ctrl-c.
Ory Band
la source
9
Existe-t-il un moyen facile de résilier prog1lorsque se prog2termine? Pensez à node srv.js & cucumberjs
JP
20
Je viens de l'essayer et cela n'a pas fonctionné comme prévu pour moi. Cependant, une légère modification a fonctionné: prog1 & prog2 ; fg c'était pour exécuter plusieurs tunnels ssh à la fois. J'espère que cela aide quelqu'un.
jnadro52
2
@ jnadro52 votre solution a pour effet que si elle prog2ne s'exécute pas immédiatement, vous reviendrez prog1au premier plan. Si cela est souhaitable, c'est ok.
Ory Band
3
Sur le shell SSH Si vous exécutez une commande comme celle-ci, il sera difficile de tuer prog1. Ctrl-c n'a pas fonctionné pour moi. Même en tuant le terminal entier, prog1 a démarré.
mercury0114
14
@ jnadro52 Un moyen de terminer les deux processus en même temps est prog1 & prog2 && kill $!.
zaboco
80

Vous pouvez utiliser wait:

some_command &
P1=$!
other_command &
P2=$!
wait $P1 $P2

Il attribue les PID du programme d'arrière-plan aux variables ( $!est le dernier PID du processus lancé), puis la waitcommande les attend. C'est bien car si vous tuez le script, il tue aussi les processus!

trusktr
la source
4
D'après mon expérience , tuer l'attente ne tue pas également les autres processus.
Quinn Comendant
1
Si je démarre des processus d'arrière-plan dans une boucle, comment puis-je attendre que chaque processus d'arrière-plan se termine avant de poursuivre l'exécution de la prochaine série de commandes. #!/usr/bin/env bash ARRAY='cat bat rat' for ARR in $ARRAY do ./run_script1 $ARR & done P1=$! wait $P1 echo "INFO: Execution of all background processes in the for loop has completed.."
Yash
@Yash Je pense que vous pouvez enregistrer les ID de processus dans un tableau, puis appeler wait sur le tableau. Je pense que vous devez utiliser ${}pour l'interpoler dans une liste de chaînes ou similaire.
trusktr
la meilleure réponse, et pour moi, tuer le script tue aussi les processus! macOS Catalina, console zsh
Michael Klishevich
67

Avec GNU Parallel http://www.gnu.org/software/parallel/, c'est aussi simple que:

(echo prog1; echo prog2) | parallel

Ou si vous préférez:

parallel ::: prog1 prog2

Apprendre encore plus:

Ole Tange
la source
4
Il convient de noter qu'il existe différentes versions de parallelavec une syntaxe différente. Par exemple, sur les dérivés Debian, le moreutilspaquet contient une commande différente appelée parallelqui se comporte assez différemment.
Joel Cross
4
vaut parallelmieux que d'utiliser &?
Optimus Prime
2
@OptimusPrime Cela dépend vraiment. GNU Parallel introduit une surcharge, mais en retour vous donne beaucoup plus de contrôle sur les tâches en cours et la sortie. Si deux tâches s'impriment en même temps, GNU Parallel s'assurera que la sortie n'est pas mélangée.
Ole Tange
1
@OptimusPrime parallelest meilleur lorsqu'il y a plus de travaux que de cœurs, auquel cas &ils exécuteraient plus d'un travail par cœur à la fois. (cf. principe du pigeonhole )
Geremia
2
@OleTange " Votre ligne de commande vous aimera pour cela. " Moi aussi. ☺
Geremia
55

Si vous voulez pouvoir exécuter et tuer facilement plusieurs processus avec ctrl-c, voici ma méthode préférée: générer plusieurs processus d'arrière-plan dans un (…)sous - shell et intercepter SIGINTpour exécuter kill 0, ce qui tuera tout ce qui est généré dans le groupe de sous-shell:

(trap 'kill 0' SIGINT; prog1 & prog2 & prog3)

Vous pouvez avoir des structures d'exécution de processus complexes, et tout se terminera par un seul ctrl-c(assurez-vous simplement que le dernier processus est exécuté au premier plan, c'est-à-dire, n'incluez pas d' &after prog1.3):

(trap 'kill 0' SIGINT; prog1.1 && prog1.2 & (prog2.1 | prog2.2 || prog2.3) & prog1.3)
Quinn Comendant
la source
C'est de loin la meilleure réponse.
Nic
10

xargs -P <n>vous permet d'exécuter des <n>commandes en parallèle.

Bien qu'il -Ps'agisse d'une option non standard, les implémentations GNU (Linux) et macOS / BSD la prennent en charge.

L'exemple suivant:

  • exécute au plus 3 commandes en parallèle à la fois,
  • avec des commandes supplémentaires démarrant uniquement à la fin d'un processus précédemment lancé.
time xargs -P 3 -I {} sh -c 'eval "$1"' - {} <<'EOF'
sleep 1; echo 1
sleep 2; echo 2
sleep 3; echo 3
echo 4
EOF

La sortie ressemble à quelque chose comme:

1   # output from 1st command 
4   # output from *last* command, which started as soon as the count dropped below 3
2   # output from 2nd command
3   # output from 3rd command

real    0m3.012s
user    0m0.011s
sys 0m0.008s

Le timing montre que les commandes ont été exécutées en parallèle (la dernière commande n'a été lancée qu'après la fin du premier des 3 originaux, mais exécutée très rapidement).

La xargscommande elle-même ne reviendra pas tant que toutes les commandes ne seront pas terminées, mais vous pouvez l'exécuter en arrière-plan en la terminant avec l'opérateur de contrôle &, puis en utilisant la fonction waitintégrée pour attendre la fin de la xargscommande entière .

{
  xargs -P 3 -I {} sh -c 'eval "$1"' - {} <<'EOF'
sleep 1; echo 1
sleep 2; echo 2
sleep 3; echo 3
echo 4
EOF
} &

# Script execution continues here while `xargs` is running 
# in the background.
echo "Waiting for commands to finish..."

# Wait for `xargs` to finish, via special variable $!, which contains
# the PID of the most recently started background process.
wait $!

Remarque:

  • BSD / macOS xargsvous oblige à préciser le nombre de commandes à exécuter en parallèle explicitement , alors que GNU xargsvous permet de spécifier -P 0d'exécuter le plus grand nombre possible en parallèle.

  • La sortie des processus exécutés en parallèle arrive au fur et à mesure de sa génération , elle sera donc entrelacée de façon imprévisible .

    • GNU parallel, comme mentionné dans la réponse d'Ole ( non fourni en standard avec la plupart des plates-formes), sérialise (regroupe) commodément la sortie par processus et offre de nombreuses fonctionnalités plus avancées.
mklement0
la source
9
#!/bin/bash
prog1 & 2> .errorprog1.log; prog2 & 2> .errorprog2.log

Redirigez les erreurs vers des journaux distincts.

fermin
la source
13
Vous devez mettre les esperluettes après les redirections et laisser de côté le point-virgule (l'esperluette remplira également la fonction d'un séparateur de commandes):prog1 2> .errorprog1.log & prog2 2> .errorprog2.log &
pause jusqu'à nouvel ordre.
le point-virgule exécute les deux commandes, vous pouvez tester de bash pour le voir fonctionner correctement;) Exemple: pwd & 2> .errorprog1.log; echo "wop" & 2> .errorprog2.log lorsque vous mettez & vous mettez le programme en arrière-plan et exécutez immédiatement la commande suivante.
fermin
2
Cela ne fonctionne pas - les erreurs ne sont pas redirigées vers le fichier. Essayez avec: ls notthere1 & 2> .errorprog1.log; ls notthere2 & 2>.errorprog2.log. Les erreurs vont à la console et les deux fichiers d'erreur sont vides. Comme le dit @Dennis Williamson, &est un séparateur, comme ;, donc (a) il doit aller à la fin de la commande (après toute redirection), et (b) vous n'avez pas besoin du ;tout :-)
psmears
8

Il existe un programme très utile qui appelle nohup.

     nohup - run a command immune to hangups, with output to a non-tty
3h4x
la source
4
nohuppar lui-même n'exécute rien en arrière-plan, et l'utilisation nohupn'est pas une exigence ou une condition préalable pour exécuter des tâches en arrière-plan. Ils sont souvent utiles ensemble mais en tant que tels, cela ne répond pas à la question.
tripleee
8

Voici une fonction que j'utilise pour exécuter au max n processus en parallèle (n = 4 dans l'exemple):

max_children=4

function parallel {
  local time1=$(date +"%H:%M:%S")
  local time2=""

  # for the sake of the example, I'm using $2 as a description, you may be interested in other description
  echo "starting $2 ($time1)..."
  "$@" && time2=$(date +"%H:%M:%S") && echo "finishing $2 ($time1 -- $time2)..." &

  local my_pid=$$
  local children=$(ps -eo ppid | grep -w $my_pid | wc -w)
  children=$((children-1))
  if [[ $children -ge $max_children ]]; then
    wait -n
  fi
}

parallel sleep 5
parallel sleep 6
parallel sleep 7
parallel sleep 8
parallel sleep 9
wait

Si max_children est défini sur le nombre de cœurs, cette fonction essaiera d'éviter les cœurs inactifs.

arnaldocan
la source
1
Un bel extrait, mais je ne trouve pas l'explication de "wait -n" sous mon bash, il dit que c'est une option invalide. faute de frappe ou j'ai raté quelque chose?
Emmanuel Devaux
1
@EmmanuelDevaux: wait -nnécessite bash4.3+ et il change la logique en attendant la fin de l' un des processus spécifiés / implicites.
mklement0
que faire si l'une des tâches a échoué, alors je veux mettre fin aux scripts?
52coder
@ 52coder, vous pouvez ajuster la fonction pour capturer un enfant en échec, quelque chose comme: "$ @" && time2 = $ (date + "% H:% M:% S") && echo "terminant $ 2 ($ time1 - $ time2 ) ... "|| erreur = 1 &. Ensuite, testez les erreurs dans la partie "si" et annulez la fonction si nécessaire
arnaldocan
7

Vous pouvez essayer ppss . ppss est assez puissant - vous pouvez même créer un mini-cluster. xargs -P peut également être utile si vous avez un lot de traitement parallèle embarrassant à faire.

ljt
la source
7

J'ai eu une situation similaire récemment où j'avais besoin d'exécuter plusieurs programmes en même temps, de rediriger leurs sorties vers des fichiers journaux séparés et d'attendre qu'ils se terminent et je me suis retrouvé avec quelque chose comme ça:

#!/bin/bash

# Add the full path processes to run to the array
PROCESSES_TO_RUN=("/home/joao/Code/test/prog_1/prog1" \
                  "/home/joao/Code/test/prog_2/prog2")
# You can keep adding processes to the array...

for i in ${PROCESSES_TO_RUN[@]}; do
    ${i%/*}/./${i##*/} > ${i}.log 2>&1 &
    # ${i%/*} -> Get folder name until the /
    # ${i##*/} -> Get the filename after the /
done

# Wait for the processes to finish
wait

Source: http://joaoperibeiro.com/execute-multiple-programs-and-redirect-their-outputs-linux/

Joaopcribeiro
la source
4

Gestionnaire de génération de processus

Bien sûr, techniquement, ce sont des processus, et ce programme devrait vraiment être appelé un gestionnaire de génération de processus, mais cela n'est dû qu'à la façon dont BASH fonctionne quand il bifurque en utilisant l'esperluette, il utilise l'appel système fork () ou peut-être clone () qui clone dans un espace mémoire séparé, plutôt que quelque chose comme pthread_create () qui partagerait la mémoire. Si BASH supportait ce dernier, chaque "séquence d'exécution" fonctionnerait de la même manière et pourrait être qualifiée de threads traditionnels tout en gagnant une empreinte mémoire plus efficace. Fonctionnellement, cependant, cela fonctionne de la même manière, bien qu'un peu plus difficile car les variables GLOBALES ne sont pas disponibles dans chaque clone de travail, d'où l'utilisation du fichier de communication interprocessus et du sémaphore du troupeau rudimentaire pour gérer les sections critiques. Le fork de BASH est bien sûr la réponse de base ici, mais j'ai l'impression que les gens le savent mais cherchent vraiment à gérer ce qui est généré plutôt que de simplement le fork et l'oublier. Cela illustre un moyen de gérer jusqu'à 200 instances de processus bifurqués accédant tous à une seule ressource. De toute évidence, c'est exagéré, mais j'ai aimé l'écrire, alors j'ai continué. Augmentez la taille de votre terminal en conséquence. J'espère que vous trouvez ça utile.

ME=$(basename $0)
IPC="/tmp/$ME.ipc"      #interprocess communication file (global thread accounting stats)
DBG=/tmp/$ME.log
echo 0 > $IPC           #initalize counter
F1=thread
SPAWNED=0
COMPLETE=0
SPAWN=1000              #number of jobs to process
SPEEDFACTOR=1           #dynamically compensates for execution time
THREADLIMIT=50          #maximum concurrent threads
TPS=1                   #threads per second delay
THREADCOUNT=0           #number of running threads
SCALE="scale=5"         #controls bc's precision
START=$(date +%s)       #whence we began
MAXTHREADDUR=6         #maximum thread life span - demo mode

LOWER=$[$THREADLIMIT*100*90/10000]   #90% worker utilization threshold
UPPER=$[$THREADLIMIT*100*95/10000]   #95% worker utilization threshold
DELTA=10                             #initial percent speed change

threadspeed()        #dynamically adjust spawn rate based on worker utilization
{
   #vaguely assumes thread execution average will be consistent
   THREADCOUNT=$(threadcount)
   if [ $THREADCOUNT -ge $LOWER ] && [ $THREADCOUNT -le $UPPER ] ;then
      echo SPEED HOLD >> $DBG
      return
   elif [ $THREADCOUNT -lt $LOWER ] ;then
      #if maxthread is free speed up
      SPEEDFACTOR=$(echo "$SCALE;$SPEEDFACTOR*(1-($DELTA/100))"|bc)
      echo SPEED UP $DELTA%>> $DBG
   elif [ $THREADCOUNT -gt $UPPER ];then
      #if maxthread is active then slow down
      SPEEDFACTOR=$(echo "$SCALE;$SPEEDFACTOR*(1+($DELTA/100))"|bc)
      DELTA=1                            #begin fine grain control
      echo SLOW DOWN $DELTA%>> $DBG
   fi

   echo SPEEDFACTOR $SPEEDFACTOR >> $DBG

   #average thread duration   (total elapsed time / number of threads completed)
   #if threads completed is zero (less than 100), default to maxdelay/2  maxthreads

   COMPLETE=$(cat $IPC)

   if [ -z $COMPLETE ];then
      echo BAD IPC READ ============================================== >> $DBG
      return
   fi

   #echo Threads COMPLETE $COMPLETE >> $DBG
   if [ $COMPLETE -lt 100 ];then
      AVGTHREAD=$(echo "$SCALE;$MAXTHREADDUR/2"|bc)
   else
      ELAPSED=$[$(date +%s)-$START]
      #echo Elapsed Time $ELAPSED >> $DBG
      AVGTHREAD=$(echo "$SCALE;$ELAPSED/$COMPLETE*$THREADLIMIT"|bc)
   fi
   echo AVGTHREAD Duration is $AVGTHREAD >> $DBG

   #calculate timing to achieve spawning each workers fast enough
   # to utilize threadlimit - average time it takes to complete one thread / max number of threads
   TPS=$(echo "$SCALE;($AVGTHREAD/$THREADLIMIT)*$SPEEDFACTOR"|bc)
   #TPS=$(echo "$SCALE;$AVGTHREAD/$THREADLIMIT"|bc)  # maintains pretty good
   #echo TPS $TPS >> $DBG

}
function plot()
{
   echo -en \\033[${2}\;${1}H

   if [ -n "$3" ];then
         if [[ $4 = "good" ]];then
            echo -en "\\033[1;32m"
         elif [[ $4 = "warn" ]];then
            echo -en "\\033[1;33m"
         elif [[ $4 = "fail" ]];then
            echo -en "\\033[1;31m"
         elif [[ $4 = "crit" ]];then
            echo -en "\\033[1;31;4m"
         fi
   fi
      echo -n "$3"
      echo -en "\\033[0;39m"
}

trackthread()   #displays thread status
{
   WORKERID=$1
   THREADID=$2
   ACTION=$3    #setactive | setfree | update
   AGE=$4

   TS=$(date +%s)

   COL=$[(($WORKERID-1)/50)*40]
   ROW=$[(($WORKERID-1)%50)+1]

   case $ACTION in
      "setactive" )
         touch /tmp/$ME.$F1$WORKERID  #redundant - see main loop
         #echo created file $ME.$F1$WORKERID >> $DBG
         plot $COL $ROW "Worker$WORKERID: ACTIVE-TID:$THREADID INIT    " good
         ;;
      "update" )
         plot $COL $ROW "Worker$WORKERID: ACTIVE-TID:$THREADID AGE:$AGE" warn
         ;;
      "setfree" )
         plot $COL $ROW "Worker$WORKERID: FREE                         " fail
         rm /tmp/$ME.$F1$WORKERID
         ;;
      * )

      ;;
   esac
}

getfreeworkerid()
{
   for i in $(seq 1 $[$THREADLIMIT+1])
   do
      if [ ! -e /tmp/$ME.$F1$i ];then
         #echo "getfreeworkerid returned $i" >> $DBG
         break
      fi
   done
   if [ $i -eq $[$THREADLIMIT+1] ];then
      #echo "no free threads" >> $DBG
      echo 0
      #exit
   else
      echo $i
   fi
}

updateIPC()
{
   COMPLETE=$(cat $IPC)        #read IPC
   COMPLETE=$[$COMPLETE+1]     #increment IPC
   echo $COMPLETE > $IPC       #write back to IPC
}


worker()
{
   WORKERID=$1
   THREADID=$2
   #echo "new worker WORKERID:$WORKERID THREADID:$THREADID" >> $DBG

   #accessing common terminal requires critical blocking section
   (flock -x -w 10 201
      trackthread $WORKERID $THREADID setactive
   )201>/tmp/$ME.lock

   let "RND = $RANDOM % $MAXTHREADDUR +1"

   for s in $(seq 1 $RND)               #simulate random lifespan
   do
      sleep 1;
      (flock -x -w 10 201
         trackthread $WORKERID $THREADID update $s
      )201>/tmp/$ME.lock
   done

   (flock -x -w 10 201
      trackthread $WORKERID $THREADID setfree
   )201>/tmp/$ME.lock

   (flock -x -w 10 201
      updateIPC
   )201>/tmp/$ME.lock
}

threadcount()
{
   TC=$(ls /tmp/$ME.$F1* 2> /dev/null | wc -l)
   #echo threadcount is $TC >> $DBG
   THREADCOUNT=$TC
   echo $TC
}

status()
{
   #summary status line
   COMPLETE=$(cat $IPC)
   plot 1 $[$THREADLIMIT+2] "WORKERS $(threadcount)/$THREADLIMIT  SPAWNED $SPAWNED/$SPAWN  COMPLETE $COMPLETE/$SPAWN SF=$SPEEDFACTOR TIMING=$TPS"
   echo -en '\033[K'                   #clear to end of line
}

function main()
{
   while [ $SPAWNED -lt $SPAWN ]
   do
      while [ $(threadcount) -lt $THREADLIMIT ] && [ $SPAWNED -lt $SPAWN ]
      do
         WID=$(getfreeworkerid)
         worker $WID $SPAWNED &
         touch /tmp/$ME.$F1$WID    #if this loops faster than file creation in the worker thread it steps on itself, thread tracking is best in main loop
         SPAWNED=$[$SPAWNED+1]
         (flock -x -w 10 201
            status
         )201>/tmp/$ME.lock
         sleep $TPS
        if ((! $[$SPAWNED%100]));then
           #rethink thread timing every 100 threads
           threadspeed
        fi
      done
      sleep $TPS
   done

   while [ "$(threadcount)" -gt 0 ]
   do
      (flock -x -w 10 201
         status
      )201>/tmp/$ME.lock
      sleep 1;
   done

   status
}

clear
threadspeed
main
wait
status
echo
Josiah DeWitt
la source
0

Votre script devrait ressembler à:

prog1 &
prog2 &
.
.
progn &
wait
progn+1 &
progn+2 &
.
.

En supposant que votre système peut prendre n tâches à la fois. utilisez wait pour exécuter uniquement n jobs à la fois.

amalik2205
la source
-1

Avec bashj ( https://sourceforge.net/projects/bashj/ ), vous devriez être en mesure d'exécuter non seulement plusieurs processus (comme d'autres l'ont suggéré) mais également plusieurs threads dans une machine virtuelle Java contrôlée à partir de votre script. Mais bien sûr, cela nécessite un JDK java. Les threads consomment moins de ressources que les processus.

Voici un code de travail:

#!/usr/bin/bashj

#!java

public static int cnt=0;

private static void loop() {u.p("java says cnt= "+(cnt++));u.sleep(1.0);}

public static void startThread()
{(new Thread(() ->  {while (true) {loop();}})).start();}

#!bashj

j.startThread()

while [ j.cnt -lt 4 ]
do
  echo "bash views cnt=" j.cnt
  sleep 0.5
done
Fil
la source