resueltos - ¿Cómo se ejecutan múltiples programas en paralelo desde un script bash?
scripts linux ejercicios resueltos (11)
Estoy tratando de escribir un archivo .sh que ejecuta muchos programas simultáneamente
Intenté esto
prog1
prog2
Pero eso corre prog1 luego espera hasta que prog1 finalice y luego se inicia prog2 ...
Entonces, ¿cómo puedo ejecutarlos en paralelo?
Aquí hay una función que uso para ejecutar el proceso max 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 está configurado en la cantidad de núcleos, esta función intentará evitar los núcleos inactivos.
Con GNU Parallel http://www.gnu.org/software/parallel/ es tan fácil como:
(echo prog1; echo prog2) | parallel
O si lo prefiere:
parallel ::: prog1 prog2
Aprende más:
- Mire el video introductorio para obtener una introducción rápida: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1
- Recorre el tutorial (man parallel_tutorial). Tu línea de comando te amará por eso.
Hay un programa muy útil que llama nohup.
nohup - run a command immune to hangups, with output to a non-tty
Puedes probar ppss . ppss es bastante poderoso, incluso puedes crear un mini-cluster. xargs -P también puede ser útil si tienes un lote de procesamiento vergonzosamente paralelo que hacer.
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 proceso lanzado ''PID), luego el comando wait
espera. Es bueno porque si matas el script, también mata los procesos.
Qué tal si:
prog1 & prog2 && fg
Esta voluntad:
- Comience
prog1
. - Envíelo a fondo, pero siga imprimiendo su salida.
- Inicie
prog2
y manténgalo en primer plano para que pueda cerrarlo conctrl-c
. - Cuando cierre
prog2
, volverá al primer plano delprog1
, por lo que también puede cerrarlo conctrl-c
.
Recientemente tuve una situación similar en la que necesitaba ejecutar múltiples programas al mismo tiempo, redirigir sus salidas a archivos de registro separados y esperar a que terminen 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/
xargs -P <n>
permite ejecutar comandos <n>
en paralelo.
Mientras -P
es una opción no estándar, las implementaciones GNU (Linux) y 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 ve algo como:
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 fue el comando que se inició solo después de que el puño del original 3 terminó, pero se ejecutó muy rápidamente).
El comando xargs
sí no regresará hasta que todos los comandos hayan finalizado, pero puede ejecutarlo en segundo plano al terminarlo con el operador de control y luego usar el comando de espera para esperar a que termine todo el comando xargs
.
{
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
xargs
requiere que especifique el recuento de comandos para ejecutar en paralelo explícitamente , mientras que losxargs
GNU le permiten especificar-P 0
para ejecutar tantos como sea posible en paralelo.La salida de los procesos que se ejecutan en paralelo llega a medida que se genera , por lo que se intercalará imprevisiblemente .
- El
parallel
GNU, como se menciona en la respuesta de Ole ( no viene de manera estándar con la mayoría de las plataformas), serializa (agrupa) convenientemente la salida por proceso y ofrece muchas más funciones avanzadas.
- El
Gerente de desove de procesos
Claro, técnicamente estos son procesos, y este programa realmente debería llamarse administrador de generación de procesos, pero esto solo se debe a la forma en que BASH funciona cuando se bifurca utilizando el signo y, usa la llamada al sistema fork () o quizás clone () que se clona en un espacio de memoria separado, en lugar de algo como pthread_create () que compartiría memoria. Si BASH soportaba este último, cada "secuencia de ejecución" funcionaría de la misma manera y podría denominarse como hilos tradicionales a la vez que obtendría una huella de memoria más eficiente. Funcionalmente, sin embargo, funciona igual, aunque es un poco más difícil ya que las variables GLOBAL no están disponibles en cada clon de trabajador, por lo tanto, el uso del archivo de comunicación entre procesos y el semáforo flock rudimentario para gestionar secciones críticas. Bifurcar de BASH, por supuesto, es la respuesta básica aquí, pero siento como si la gente lo supiera, pero realmente está buscando administrar lo que se genera en lugar de simplemente bifurcarlo y olvidarlo. Esto demuestra una forma de administrar hasta 200 instancias de procesos bifurcados, todos accediendo a un único recurso. Claramente esto es excesivo pero disfruté escribiéndolo 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
#!/bin/bash
prog1 & 2> .errorprog1.log; prog2 & 2> .errorprog2.log
Redirigir los errores para separar los registros.
prog1 &
prog2 &