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 procesador de 4 núcleos brillante y agradable. Me gustaría poder ejecutar hasta 4 do-something
a la vez.
El enfoque ingenuo parece ser:
for foo in `some-command`
do
do-something $foo &
done
Esto ejecutará todos los mensajes de correodo-something
electrónico a la vez, pero hay un par de desventajas, principalmente que hacer algo también puede tener algunas E / S significativas que, al ejecutarlas todas a la vez, podrían ralentizarse un poco. El otro problema es que este bloque de código regresa inmediatamente, por lo que no hay forma de hacer otro trabajo cuando todos los do-something
correos electrónicos están terminados.
¿Cómo escribirías este bucle para que siempre haya X do-something
corriendo a la vez?
Respuestas:
Dependiendo de lo que quiera 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.
fuente
find [...] -print0
yxargs -0
.cpus=$(getconf _NPROCESSORS_ONLN)
--max-procs=0
para obtener tantos procesos como sea posible?--max-procs=0
es más como el intento del interrogador (iniciar tantos procesos como argumentos).Con GNU Parallel http://www.gnu.org/software/parallel/ puede escribir:
GNU Parallel también admite la ejecución de trabajos en equipos remotos. Esto ejecutará uno por núcleo de CPU en las computadoras remotas, incluso si tienen un número diferente de núcleos:
Un ejemplo más avanzado: aquí enumeramos los 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 (por ejemplo, foo.jpeg -> foo.out). Queremos ejecutar my_script una vez por 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 termine my_script, queremos que foo.out se transfiera de nuevo 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:
Vea los videos para ver más ejemplos: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1
fuente
find
comando para generar una lista de archivos, porque no solo previene el problema cuando hay un espacio dentro de un nombre de archivo que aparece,for i in ...; do
sino que find también puede hacer lofind -name \*.extension1 -or -name \*.extension2
que el paralelo de GNU {.} Puede manejar muy bien.cat
es, por supuesto, inútil.fuente
Aquí una solución alternativa que puede insertarse en .bashrc y usarse para un revestimiento diario:
function pwait() { while [ $(jobs -p | wc -l) -ge $1 ]; do sleep 1 done }
Para usarlo, todo lo que hay que hacer es poner
&
después de los trabajos y una llamada pwait, el parámetro da el número de procesos paralelos:for i in *; do do_something $i & pwait 10 done
Sería más agradable usarlo en
wait
lugar de estar ocupado esperando la salida dejobs -p
, pero no parece haber una solución obvia para esperar hasta que se termine cualquiera de los trabajos en lugar de todos.fuente
En lugar de un bash simple, use un Makefile, luego especifique el número de trabajos simultáneos
make -jX
donde X es el número de trabajos a ejecutar a la vez.O puede usar
wait
("man wait
"): lanzar varios procesos secundarios, llamarwait
- se cerrará 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 de
wait
comprobar qué contiene la variable.fuente
¿Quizás probar una utilidad de paralelización en lugar de reescribir el ciclo? Soy un gran fan de xjobs. Utilizo xjobs todo el tiempo para copiar archivos en masa a través de nuestra red, generalmente al configurar un nuevo servidor de base de datos. http://www.maier-komor.de/xjobs.html
fuente
Si está 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 makey luego solo corre
para ejecutar como máximo NUMBER comandos en paralelo.
fuente
Si bien hacer esto correctamente
bash
es probablemente imposible, puede hacer un semi-derecho con bastante facilidad.bstark
dio una aproximación justa del derecho, pero el suyo tiene los siguientes defectos: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 es fácilmente adaptable para verificar también el código de salida de cada trabajo a medida que termina, de modo que pueda advertir al usuario si un trabajo falla o establecer un código de salida de
scheduleAll
acuerdo con la cantidad de trabajos que fallaron, o algo así.El problema con este código es solo eso:
Una solución que se encargue de este último problema tendría que utilizar
kill -0
para sondear si alguno de los procesos ha desaparecido en lugar delwait
y programar el siguiente trabajo. Sin embargo, eso introduce un pequeño problema nuevo: tiene una condición de carrera entre la finalización de un trabajo y lakill -0
verificación de si ha finalizado. Si el trabajo terminó y otro proceso en su sistema se inicia al mismo tiempo, tomando un PID aleatorio que es el del trabajo que acaba de terminar,kill -0
no notará que su trabajo ha terminado y las cosas se romperán nuevamente.No es posible una solución perfecta en
bash
.fuente
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 }
utilizando:
fuente
make -j
es inteligente, pero sin explicación y con esa cantidad de código Awk de solo escritura, me abstengo de votar a favor.El proyecto en el que trabajo usa el comando wait para controlar los procesos de shell paralelo (ksh en realidad). Para abordar sus inquietudes sobre IO, en un sistema operativo moderno, es posible que la ejecución en paralelo 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 de la memoria caché del disco del sistema operativo. 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.
fuente
Esto puede ser lo suficientemente bueno 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
fuente
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
fuente
Realmente tarde para la fiesta aquí, 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, comen cpu en bucles ocupados o dependen de dependencias externas (por ejemplo, GNU
parallel
).Con inspiración para el manejo de procesos muertos / zombies , 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:
Para el manejo de salida por proceso, se
$$
podría usar para registrar 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[@]}"
Salida:
fuente
Puede usar un bucle for anidado simple (sustituya los números enteros apropiados por N y M 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, cada ronda ejecuta N trabajos en paralelo. Puede hacer que N sea igual al número de CPU que tiene.
fuente
Mi solución para mantener siempre un número determinado de procesos en ejecución, realizar un seguimiento de los errores y manejar procesos ubnterruptibles / zombies:
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"
fuente
$ DOMAINS = "lista de algunos dominios en comandos" para foo in
some-command
doeval `some-command for $DOMAINS` & job[$i]=$! i=$(( i + 1))
hecho
Ndomains =
echo $DOMAINS |wc -w
para i en $ (seq 1 1 $ Ndomains) echo "esperar $ {trabajo [$ i]}" esperar "$ {trabajo [$ i]}" hecho
en este concepto funcionará para paralelizar. Lo importante es que la última línea de eval es '&' que pondrá los comandos en segundo plano.
fuente