¿Cómo ejecutas múltiples programas en paralelo desde un script bash?

245

Estoy tratando de escribir un archivo .sh que ejecute muchos programas simultáneamente

Probé esto

prog1 
prog2

Pero eso ejecuta prog1 luego espera hasta que termine prog1 y luego comienza prog2 ...

Entonces, ¿cómo puedo ejecutarlos en paralelo?

Betamoo
fuente

Respuestas:

216
prog1 &
prog2 &
psmears
fuente
49
¡No te olvides de la wait! Sí, en bash puedes esperar los procesos secundarios del script.
Dummy00001
55
Otra opción es usar nohuppara evitar que el programa se elimine cuando el shell cuelga.
Philipp
@liang: Sí, también funcionará con tres o más programas.
psmears el
302

Qué tal si:

prog1 & prog2 && fg

Esta voluntad:

  1. Inicio prog1.
  2. Envíelo al fondo, pero siga imprimiendo su salida.
  3. Comience prog2y manténgalo en primer plano , para que pueda cerrarlo ctrl-c.
  4. Al cerrar prog2, volverá a prog1's primer plano , por lo que puede también cerca con ctrl-c.
Ory Band
fuente
99
¿Hay una manera fácil de terminar prog1cuando prog2termina? Piense en node srv.js & cucumberjs
JP
20
Solo intenté esto, y no funcionó como se esperaba para mí. Sin embargo, una ligera modificación funcionó: prog1 & prog2 ; fg esto fue para ejecutar múltiples túneles ssh a la vez. Espero que esto ayude a alguien.
jnadro52
2
@ jnadro52 su solución tiene el efecto de que si prog2no se ejecuta de inmediato, volverá a tener prog1el primer plano. Si esto es deseable, entonces está bien.
Ory Band
3
En el shell SSH'ed Si ejecuta un comando como este, será difícil matar prog1. Ctrl-c no funcionó para mí. Incluso matar todo el terminal dejó prog1 en ejecución.
mercury0114
14
@ jnadro52 Una forma de terminar ambos procesos a la vez es prog1 & prog2 && kill $!.
zaboco
79

Puedes usar wait:

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

Asigna los PID del programa en segundo plano a las variables ( $!es el último PID del proceso iniciado), luego el waitcomando los espera. Es bueno porque si matas el script, también mata los procesos.

trusktr
fuente
44
En mi experiencia , matar a la espera no también mata a los otros procesos.
Quinn Commandado el
1
Si estoy iniciando procesos en segundo plano en un bucle, ¿cómo puedo esperar a que se complete cada proceso en segundo plano antes de seguir adelante con la ejecución del siguiente conjunto de comandos. #!/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 Creo que puede guardar las ID de proceso en una matriz y luego llamar a wait en la matriz. Creo que debe usar ${}para interpolarlo en una lista de cadenas o similar.
trusktr
la mejor respuesta, y para mí, matar el guión también mata los procesos. macOS Catalina, consola zsh
Michael Klishevich
67

Con GNU Parallel http://www.gnu.org/software/parallel/ es tan fácil como:

(echo prog1; echo prog2) | parallel

O si lo prefieres:

parallel ::: prog1 prog2

Aprende más:

Ole Tange
fuente
44
Vale la pena señalar que hay diferentes versiones de parallelcon diferentes sintaxis. Por ejemplo, en los derivados de Debian, el moreutilspaquete contiene un comando diferente llamado parallelque se comporta de manera bastante diferente.
Joel Cross
44
es parallelmejor que usar &?
Optimus Prime
2
@OptimusPrime Realmente depende. GNU Parallel introduce algo de sobrecarga, pero a cambio le brinda mucho más control sobre los trabajos en ejecución y la salida. Si se imprimen dos trabajos al mismo tiempo, GNU Parallel se asegurará de que la salida no se mezcle.
Ole Tange
1
@OptimusPrime paralleles mejor cuando hay más trabajos que núcleos, en cuyo caso &ejecutaría más de un trabajo por núcleo a la vez. (cf. principio del casillero )
Geremia
2
@OleTange " Tu línea de comando te amará por eso " . Yo también. ☺
Geremia
55

Si desea poder ejecutar y eliminar fácilmente múltiples procesos ctrl-c, este es mi método favorito: generar múltiples procesos en segundo plano en una (…)subshell y capturar SIGINTpara ejecutar kill 0, lo que matará todo lo generado en el grupo de subshell:

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

Puede tener estructuras de ejecución de proceso complejas, y todo se cerrará con una sola ctrl-c(solo asegúrese de que el último proceso se ejecute en primer plano, es decir, no incluya un &después prog1.3):

(trap 'kill 0' SIGINT; prog1.1 && prog1.2 & (prog2.1 | prog2.2 || prog2.3) & prog1.3)
Quinn Commandado
fuente
Esta es la mejor respuesta con diferencia.
Nic
10

xargs -P <n>le permite ejecutar <n>comandos en paralelo.

Si bien -Pes una opción no estándar, tanto las implementaciones GNU (Linux) como macOS / BSD lo admiten.

El siguiente ejemplo:

  • ejecuta como máximo 3 comandos en paralelo a la vez,
  • con comandos adicionales que comienzan solo cuando finaliza un proceso iniciado anteriormente.
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

El resultado se parece a:

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

El tiempo muestra que los comandos se ejecutaron en paralelo (el último comando se lanzó solo después de que finalizó el primero de los 3 originales, pero se ejecutó muy rápidamente).

El xargscomando en sí no volverá hasta que todos los comandos hayan terminado, pero puede ejecutarlo en segundo plano al terminarlo con el operador de control &y luego usar el waitincorporado para esperar xargsa que termine todo el comando.

{
  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 $!

Nota:

  • BSD / macOS xargsrequiere que especifique el recuento de comandos para ejecutar en paralelo explícitamente , mientras que GNU le xargspermite especificar -P 0ejecutar tantos como sea posible en paralelo.

  • La salida de los procesos ejecutados en paralelo llega a medida que se genera , por lo que se intercalará de forma impredecible .

    • GNU parallel, como se menciona en la respuesta de Ole ( no viene estándar con la mayoría de las plataformas), serializa (agrupa) convenientemente la salida por proceso y ofrece muchas características más avanzadas.
mklement0
fuente
9
#!/bin/bash
prog1 & 2> .errorprog1.log; prog2 & 2> .errorprog2.log

Redirigir errores para separar los registros.

fermín
fuente
13
Debe colocar los signos de unión después de las redirecciones y omitir el punto y coma (el signo de unión también realizará la función de un separador de comandos):prog1 2> .errorprog1.log & prog2 2> .errorprog2.log &
pausa hasta nuevo aviso.
el punto y coma ejecuta ambos comandos, puede probar de bash para ver que funciona bien;) Ejemplo: pwd & 2> .errorprog1.log; echo "wop" & 2> .errorprog2.log cuando coloca y coloca el programa en segundo plano e inmediatamente ejecuta el siguiente comando.
fermin
2
No funciona: los errores no se redirigen al archivo. Pruebe con: ls notthere1 & 2> .errorprog1.log; ls notthere2 & 2>.errorprog2.log. Los errores van a la consola y ambos archivos de errores están vacíos. Como dice @Dennis Williamson, &es un separador, ;por lo que (a) debe ir al final del comando (después de cualquier redirección), y (b) no necesita ;nada :-)
psmears
8

Hay un programa muy útil que llama nohup.

     nohup - run a command immune to hangups, with output to a non-tty
3h4x
fuente
44
nohuppor sí solo no ejecuta nada en segundo plano, y el uso nohupno es un requisito o requisito previo para ejecutar tareas en segundo plano. A menudo son útiles juntos, pero como tal, esto no responde la pregunta.
tripleee
8

Aquí hay una función que uso para ejecutar al máximo el proceso n en paralelo (n = 4 en el ejemplo):

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 se establece en el número de núcleos, esta función intentará evitar los núcleos inactivos.

arnaldocan
fuente
1
Bonito fragmento, pero no puedo encontrar la explicación de "esperar -n" debajo de mi bash, dice que es una opción no válida. error tipográfico o me perdí algo?
Emmanuel Devaux
1
@EmmanuelDevaux: wait -nrequiere bash4.3+ y cambia la lógica para esperar a que finalice cualquiera de los procesos especificados / implícitos.
mklement0
¿Qué pasa si una de las tareas falla, entonces quiero finalizar los scripts?
52coder
@ 52coder, puede ajustar la función para capturar un hijo fallido, algo así como: "$ @" && time2 = $ (date + "% H:% M:% S") && echo "finalizando $ 2 ($ time1 - $ time2 ) ... "|| error = 1 &. Luego, pruebe el error en la parte "if" y anule la función si es necesario
arnaldocan
7

Puedes probar ppss . ppss es bastante poderoso, incluso puedes crear un mini-cluster. xargs -P también puede ser útil si tienes que hacer un lote de procesamiento embarazosamente paralelo.

ljt
fuente
7

Recientemente tuve una situación similar en la que necesitaba ejecutar varios programas al mismo tiempo, redirigir sus salidas a archivos de registro separados y esperar a que terminaran y terminé con algo así:

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

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

Joaopcribeiro
fuente
4

Gerente de proceso de desove

Claro, técnicamente estos son procesos, y este programa realmente debería llamarse un administrador de generación de procesos, pero esto solo se debe a la forma en que BASH funciona cuando se bifurca usando el ampersand, usa la llamada al sistema fork () o quizás clone () que clona en un espacio de memoria separado, en lugar de algo como pthread_create () que compartiría memoria. Si BASH admitiera lo último, cada "secuencia de ejecución" funcionaría de la misma manera y podría denominarse subprocesos tradicionales al tiempo que obtendría una huella de memoria más eficiente. Funcionalmente, sin embargo, funciona de la misma manera, aunque un poco más difícil ya que las variables GLOBALES no están disponibles en cada clon de trabajo, por lo tanto, el uso del archivo de comunicación entre procesos y el semáforo rudimentario de bandadas para gestionar secciones críticas. Bifurcar desde BASH, por supuesto, es la respuesta básica aquí, pero siento que la gente lo sabe, pero realmente está buscando administrar lo que se genera en lugar de simplemente bifurcarlo y olvidarlo. Esto demuestra una manera de administrar hasta 200 instancias de procesos bifurcados, todos accediendo a un solo recurso. Claramente, esto es excesivo, pero disfruté escribirlo, así que seguí. Aumente el tamaño de su terminal en consecuencia. Espero que encuentres esto útil.

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
fuente
0

Su guión debería verse así:

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

Asumiendo que su sistema puede tomar n trabajos a la vez. use esperar para ejecutar solo n trabajos a la vez.

amalik2205
fuente
-1

Con bashj ( https://sourceforge.net/projects/bashj/ ), debería poder ejecutar no solo múltiples procesos (como lo sugirieron otros) sino también múltiples subprocesos en una JVM controlada desde su script. Pero, por supuesto, esto requiere un Java JDK. Los subprocesos consumen menos recursos que los procesos.

Aquí hay un código de trabajo:

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