web-dev-qa-db-fra.com

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

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

J'ai essayé ça

prog1 
prog2

Mais cela lance prog1 puis attend que prog1 se termine, puis commence prog2 ...

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

201
Betamoo
prog1 &
prog2 &
178
psmears

Que diriez-vous:

_prog1 & prog2 && fg
_

Cette volonté:

  1. Démarrer _prog1_.
  2. Envoyez-le en arrière-plan, mais continuez à imprimer sa sortie.
  3. Démarrez _prog2_ et conservez-le au premier plan pour pouvoir le fermer avec _ctrl-c_.
  4. Lorsque vous fermez _prog2_, vous revenez à l'avant-plan de _prog1_ afin que vous puissiez également le fermer avec _ctrl-c_.
266
Ory Band

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:

59
Ole Tange

Vous pouvez utiliser wait:

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

Il assigne les PID du programme d'arrière-plan aux variables ($! est le dernier PID du processus lancé), puis la commande wait les attend. C'est bien parce que si vous tuez le script, il tue également les processus!

54
trusktr

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 en arrière-plan dans un sous-shell (…) et intercepter SIGINT pour exécuter kill 0, qui tuera tout ce qui est apparu 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 fermera avec un seul ctrl-c (assurez-vous simplement que le dernier processus est exécuté au premier plan, c'est-à-dire, n'incluez pas de & après prog1.3):

(trap 'kill 0' SIGINT; prog1.1 && prog1.2 & (prog2.1 | prog2.2 || prog2.3) & prog1.3)
21
Quinn Comendant
#!/bin/bash
prog1 & 2> .errorprog1.log; prog2 & 2> .errorprog2.log

Rediriger les erreurs pour séparer les journaux.

9
fermin

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

     Nohup - run a command immune to hangups, with output to a non-tty
8
3h4x

Voici une fonction que j'utilise pour exécuter max n process 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.

7
arnaldocan

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.

7
ljt

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

Bien que -P soit 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 ne démarrant qu’à la fin d’un processus lancé précédemment.
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 aime:

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 minutage montre que les commandes ont été exécutées en parallèle (la dernière commande n'a été lancée qu'après le premier des 3 originaux, mais elle a été exécutée très rapidement).

La commande xargs elle-même ne sera renvoyée que lorsque toutes les commandes seront 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 le paramètre intégré wait pour attendre. toute la commande xargs pour terminer.

{
  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 xargs vous oblige à spécifier explicitement le nombre de commandes à exécuter en parallèle , tandis que GNU xargs vous permet de spécifier -P 0 pour en exécuter autant que possible en parallèle.

  • La sortie des processus exécutés en parallèle arrive lorsqu’elle est générée , elle sera donc entrelacée de façon imprévisible .

    • GNU parallel, comme mentionné dans réponse d'Ole (est-ce que ne vient pas en standard avec la plupart des plateformes), commodément sérialise (groupes) la sortie par processus et offre de nombreuses fonctionnalités plus avancées.
7
mklement0

J'ai eu récemment une situation similaire dans laquelle je devais exécuter plusieurs programmes en même temps, rediriger leurs sorties vers des fichiers journaux séparés et attendre qu'ils se terminent et j'ai fini 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/

7
Joaopcribeiro

Gestionnaire de génération de processus

Bien sûr, techniquement, ce sont des processus, et ce programme devrait vraiment s'appeler un gestionnaire de génération de processus, mais cela est dû uniquement à la façon dont BASH fonctionne lorsqu'il utilise une 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 prenait en charge 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 obtenant une empreinte mémoire plus efficace. Sur le plan fonctionnel, toutefois, il fonctionne de la même manière, bien qu’il soit un peu plus difficile étant donné que les variables GLOBAL ne sont pas disponibles dans chaque clone d’ouvriers, d’où l’utilisation du fichier de communication interprocessus et du sémaphore rudimentaire flock pour gérer les sections critiques. Forking from BASH est bien sûr la solution de base ici, mais j’ai l’impression que les gens le savent, mais cherchent vraiment à gérer ce qui est engendré plutôt que de simplement le fourrer et l’oublier. Cela montre un moyen de gérer jusqu'à 200 instances de processus forkés accédant toutes à une seule ressource. C’est clair que c’est exagéré, mais j’ai aimé écrire et 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
3
Josiah DeWitt