Paralelizar el script Bash con el número máximo de procesos
-
09-06-2019 - |
Pregunta
Digamos que tengo un bucle en Bash:
for foo in `some-command`
do
do-something $foo
done
do-something
está vinculado a la CPU y tengo un bonito y brillante procesador de 4 núcleos.Me gustaría poder correr hasta 4 do-something
Es de inmediato.
El enfoque ingenuo parece ser:
for foo in `some-command`
do
do-something $foo &
done
Esto se ejecutará todo do-something
s a la vez, pero hay un par de desventajas, principalmente que hacer algo también puede tener algunas E/S significativas que funcionan todo de inmediato podría disminuir un poco la velocidad.El otro problema es que este bloque de código regresa inmediatamente, por lo que no hay manera de hacer otro trabajo cuando todos los do-something
Estamos terminados.
¿Cómo escribirías este bucle para que siempre haya X? do-something
¿Está funcionando a la vez?
Solución
Dependiendo de lo que quieras hacer, xargs también puede ayudar (aquí:convertir documentos con pdf2ps):
cpus=$( ls -d /sys/devices/system/cpu/cpu[[:digit:]]* | wc -w )
find . -name \*.pdf | xargs --max-args=1 --max-procs=$cpus pdf2ps
De los documentos:
--max-procs=max-procs
-P max-procs
Run up to max-procs processes at a time; the default is 1.
If max-procs is 0, xargs will run as many processes as possible at a
time. Use the -n option with -P; otherwise chances are that only one
exec will be done.
Otros consejos
Con GNU Paralelo http://www.gnu.org/software/parallel/ puedes escribir:
some-command | parallel do-something
GNU Parallel también admite la ejecución de trabajos en computadoras remotas.Esto ejecutará uno por núcleo de CPU en las computadoras remotas, incluso si tienen una cantidad diferente de núcleos:
some-command | parallel -S server1,server2 do-something
Un ejemplo más avanzado:Aquí tenemos una lista de archivos en los que queremos que se ejecute my_script.Los archivos tienen extensión (tal vez .jpeg).Queremos que la salida de my_script se coloque junto a los archivos en basename.out (p. ej.foo.jpeg -> foo.out).Queremos ejecutar my_script una vez para cada núcleo que tenga la computadora y también queremos ejecutarlo en la computadora local.Para las computadoras remotas queremos que el archivo se procese y se transfiera a la computadora dada.Cuando my_script finalice, queremos que foo.out se vuelva a transferir y luego queremos que foo.jpeg y foo.out se eliminen de la computadora remota:
cat list_of_files | \
parallel --trc {.}.out -S server1,server2,: \
"my_script {} > {.}.out"
GNU Parallel se asegura de que la salida de cada trabajo no se mezcle, por lo que puede usar la salida como entrada para otro programa:
some-command | parallel do-something | postprocess
Vea los videos para más ejemplos: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1
maxjobs=4 parallelize () { while [ $# -gt 0 ] ; do jobcnt=(`jobs -p`) if [ ${#jobcnt[@]} -lt $maxjobs ] ; then do-something $1 & shift else sleep 1 fi done wait } parallelize arg1 arg2 "5 args to third job" arg4 ...
En lugar de un simple bash, use un Makefile, luego especifique el número de trabajos simultáneos con make -jX
donde X es el número de trabajos a ejecutar a la vez.
O puedes usar wait
("man wait
"):lanzar varios procesos secundarios, llamar wait
- Saldrá cuando finalicen los procesos secundarios.
maxjobs = 10
foreach line in `cat file.txt` {
jobsrunning = 0
while jobsrunning < maxjobs {
do job &
jobsrunning += 1
}
wait
}
job ( ){
...
}
Si necesita almacenar el resultado del trabajo, asigne su resultado a una variable.Después wait
simplemente verifica lo que contiene la variable.
Aquí hay una solución alternativa que se puede insertar en .bashrc y usar para una línea diaria:
function pwait() {
while [ $(jobs -p | wc -l) -ge $1 ]; do
sleep 1
done
}
Para utilizarlo basta con poner &
después de los trabajos y una llamada pwait, el parámetro proporciona el número de procesos paralelos:
for i in *; do
do_something $i &
pwait 10
done
Sería mejor usarlo wait
en lugar de estar ocupado esperando la salida de jobs -p
, pero no parece haber una solución obvia para esperar hasta que se complete alguno de los trabajos dados en lugar de todos.
¿Quizás probar con una utilidad de paralelización en lugar de reescribir el bucle?Soy un gran admirador de xjobs.Utilizo xjobs todo el tiempo para copiar archivos en masa a través de nuestra red, generalmente cuando configuro un nuevo servidor de base de datos.http://www.maier-komor.de/xjobs.html
Mientras hago esto bien en bash
Probablemente sea imposible, puedes hacer un semi-derecho con bastante facilidad. bstark
dio una buena aproximación al derecho, pero tiene los siguientes defectos:
- División de palabras:No puede pasarle ningún trabajo que utilice cualquiera de los siguientes caracteres en sus argumentos:espacios, tabulaciones, nuevas líneas, estrellas, signos de interrogación.Si lo hace, las cosas se romperán, posiblemente de forma inesperada.
- Depende del resto de su secuencia de comandos para no poner nada en segundo plano.Si lo hace, o luego agrega algo al script que se envía en segundo plano porque olvidó que no tenía permiso para usar trabajos en segundo plano debido a su fragmento, las cosas se romperán.
Otra aproximación que no tiene estos defectos es la siguiente:
scheduleAll() {
local job i=0 max=4 pids=()
for job; do
(( ++i % max == 0 )) && {
wait "${pids[@]}"
pids=()
}
bash -c "$job" & pids+=("$!")
done
wait "${pids[@]}"
}
Tenga en cuenta que este se adapta fácilmente para verificar también el código de salida de cada trabajo cuando finaliza, de modo que pueda advertir al usuario si un trabajo falla o establecer un código de salida para scheduleAll
según la cantidad de trabajos que fracasaron, o algo así.
El problema con este código es solo ese:
- Programa cuatro (en este caso) trabajos a la vez y luego espera a que finalicen los cuatro.Es posible que algunos se realicen antes que otros, lo que hará que el siguiente lote de cuatro trabajos espere hasta que se complete el más largo del lote anterior.
Una solución que solucione este último problema tendría que utilizar kill -0
para sondear si alguno de los procesos ha desaparecido en lugar del wait
y programar el siguiente trabajo.Sin embargo, eso introduce un pequeño problema nuevo:tiene una condición de carrera entre el final de un trabajo y el kill -0
comprobando si ha terminado.Si el trabajo finalizó y otro proceso en su sistema se inicia al mismo tiempo, tomando un PID aleatorio que resulta ser el del trabajo que acaba de finalizar, el kill -0
No notarás que tu trabajo ha terminado y las cosas se romperán nuevamente.
Una solución perfecta no es posible en bash
.
Si estás familiarizado con el make
comando, la mayoría de las veces puede expresar la lista de comandos que desea ejecutar como un archivo MAKE.Por ejemplo, si necesita ejecutar $SOME_COMMAND en archivos *.input, cada uno de los cuales produce *.output, puede usar el archivo MAKE
INPUT = a.input b.input OUTPUT = $(INPUT:.input=.output) %.output : %.input $(SOME_COMMAND) $< $@ all: $(OUTPUT)
y luego simplemente corre
make -j<NUMBER>
para ejecutar como máximo NUMBER comandos en paralelo.
función para bash:
parallel ()
{
awk "BEGIN{print \"all: ALL_TARGETS\\n\"}{print \"TARGET_\"NR\":\\n\\t@-\"\$0\"\\n\"}END{printf \"ALL_TARGETS:\";for(i=1;i<=NR;i++){printf \" TARGET_%d\",i};print\"\\n\"}" | make $@ -f - all
}
usando:
cat my_commands | parallel -j 4
El proyecto en el que trabajo utiliza el esperar comando para controlar procesos de shell paralelos (ksh en realidad).Para abordar sus inquietudes sobre IO, en un sistema operativo moderno, es posible que la ejecución paralela en realidad aumente la eficiencia.Si todos los procesos leen los mismos bloques en el disco, solo el primer proceso tendrá que llegar al hardware físico.Los otros procesos a menudo podrán recuperar el bloque del caché del disco del sistema operativo en la memoria.Obviamente, leer desde la memoria es varios órdenes de magnitud más rápido que leer desde el disco.Además, el beneficio no requiere cambios de codificación.
Esto podría ser suficiente para la mayoría de los propósitos, pero no es óptimo.
#!/bin/bash
n=0
maxjobs=10
for i in *.m4a ; do
# ( DO SOMETHING ) &
# limit jobs
if (( $(($((++n)) % $maxjobs)) == 0 )) ; then
wait # wait until all have finished (not optimal, but most times good enough)
echo $n wait
fi
done
En realidad Llegué tarde a la fiesta, pero aquí hay otra solución.
Muchas soluciones no manejan espacios/caracteres especiales en los comandos, no mantienen N trabajos ejecutándose en todo momento, consumen CPU en bucles ocupados o dependen de dependencias externas (p. ej.ÑU parallel
).
Con inspiración para el manejo de procesos muertos/zombis, aquí hay una solución bash pura:
function run_parallel_jobs {
local concurrent_max=$1
local callback=$2
local cmds=("${@:3}")
local jobs=( )
while [[ "${#cmds[@]}" -gt 0 ]] || [[ "${#jobs[@]}" -gt 0 ]]; do
while [[ "${#jobs[@]}" -lt $concurrent_max ]] && [[ "${#cmds[@]}" -gt 0 ]]; do
local cmd="${cmds[0]}"
cmds=("${cmds[@]:1}")
bash -c "$cmd" &
jobs+=($!)
done
local job="${jobs[0]}"
jobs=("${jobs[@]:1}")
local state="$(ps -p $job -o state= 2>/dev/null)"
if [[ "$state" == "D" ]] || [[ "$state" == "Z" ]]; then
$callback $job
else
wait $job
$callback $job $?
fi
done
}
Y uso de muestra:
function job_done {
if [[ $# -lt 2 ]]; then
echo "PID $1 died unexpectedly"
else
echo "PID $1 exited $2"
fi
}
cmds=( \
"echo 1; sleep 1; exit 1" \
"echo 2; sleep 2; exit 2" \
"echo 3; sleep 3; exit 3" \
"echo 4; sleep 4; exit 4" \
"echo 5; sleep 5; exit 5" \
)
# cpus="$(getconf _NPROCESSORS_ONLN)"
cpus=3
run_parallel_jobs $cpus "job_done" "${cmds[@]}"
La salida:
1
2
3
PID 56712 exited 1
4
PID 56713 exited 2
5
PID 56714 exited 3
PID 56720 exited 4
PID 56724 exited 5
Para manejo de salida por proceso $$
podría usarse para iniciar sesión en un archivo, por ejemplo:
function job_done {
cat "$1.log"
}
cmds=( \
"echo 1 \$\$ >\$\$.log" \
"echo 2 \$\$ >\$\$.log" \
)
run_parallel_jobs 2 "job_done" "${cmds[@]}"
Producción:
1 56871
2 56872
Puede utilizar un bucle for anidado simple (sustituya N y M por los números enteros apropiados a continuación):
for i in {1..N}; do
(for j in {1..M}; do do_something; done & );
done
Esto ejecutará do_something N*M veces en M rondas, y cada ronda ejecutará N trabajos en paralelo.Puedes hacer que N sea igual a la cantidad de CPU que tienes.
Así es como logré resolver este problema en un script bash:
#! /bin/bash
MAX_JOBS=32
FILE_LIST=($(cat ${1}))
echo Length ${#FILE_LIST[@]}
for ((INDEX=0; INDEX < ${#FILE_LIST[@]}; INDEX=$((${INDEX}+${MAX_JOBS})) ));
do
JOBS_RUNNING=0
while ((JOBS_RUNNING < MAX_JOBS))
do
I=$((${INDEX}+${JOBS_RUNNING}))
FILE=${FILE_LIST[${I}]}
if [ "$FILE" != "" ];then
echo $JOBS_RUNNING $FILE
./M22Checker ${FILE} &
else
echo $JOBS_RUNNING NULL &
fi
JOBS_RUNNING=$((JOBS_RUNNING+1))
done
wait
done
Mi solución para mantener siempre una determinada cantidad de procesos en ejecución, realizar un seguimiento de los errores y manejar procesos ininterrumpibles/zombis:
function log {
echo "$1"
}
# Take a list of commands to run, runs them sequentially with numberOfProcesses commands simultaneously runs
# Returns the number of non zero exit codes from commands
function ParallelExec {
local numberOfProcesses="${1}" # Number of simultaneous commands to run
local commandsArg="${2}" # Semi-colon separated list of commands
local pid
local runningPids=0
local counter=0
local commandsArray
local pidsArray
local newPidsArray
local retval
local retvalAll=0
local pidState
local commandsArrayPid
IFS=';' read -r -a commandsArray <<< "$commandsArg"
log "Runnning ${#commandsArray[@]} commands in $numberOfProcesses simultaneous processes."
while [ $counter -lt "${#commandsArray[@]}" ] || [ ${#pidsArray[@]} -gt 0 ]; do
while [ $counter -lt "${#commandsArray[@]}" ] && [ ${#pidsArray[@]} -lt $numberOfProcesses ]; do
log "Running command [${commandsArray[$counter]}]."
eval "${commandsArray[$counter]}" &
pid=$!
pidsArray+=($pid)
commandsArrayPid[$pid]="${commandsArray[$counter]}"
counter=$((counter+1))
done
newPidsArray=()
for pid in "${pidsArray[@]}"; do
# Handle uninterruptible sleep state or zombies by ommiting them from running process array (How to kill that is already dead ? :)
if kill -0 $pid > /dev/null 2>&1; then
pidState=$(ps -p$pid -o state= 2 > /dev/null)
if [ "$pidState" != "D" ] && [ "$pidState" != "Z" ]; then
newPidsArray+=($pid)
fi
else
# pid is dead, get it's exit code from wait command
wait $pid
retval=$?
if [ $retval -ne 0 ]; then
log "Command [${commandsArrayPid[$pid]}] failed with exit code [$retval]."
retvalAll=$((retvalAll+1))
fi
fi
done
pidsArray=("${newPidsArray[@]}")
# Add a trivial sleep time so bash won't eat all CPU
sleep .05
done
return $retvalAll
}
Uso:
cmds="du -csh /var;du -csh /tmp;sleep 3;du -csh /root;sleep 10; du -csh /home"
# Execute 2 processes at a time
ParallelExec 2 "$cmds"
# Execute 4 processes at a time
ParallelExec 4 "$cmds"
$ Domains = "Lista de algún dominio en los comandos" para Foo en some-command
hacer
eval `some-command for $DOMAINS` &
job[$i]=$!
i=$(( i + 1))
hecho
Ndominios=echo $DOMAINS |wc -w
para i en $ (seq 1 1 $ ndomains) hacer eco "espera $ {trabajo [$ i]}" espera "$ {trabajo [$ i]}" hecho
en este concepto funcionará para el paralelismo.Lo importante es la última línea de evaluación '&' que pondrá los comandos en fondos.