2008-09-01 31 views
75

Digamos que tengo un bucle en Bash:guión paralelizar Bash con número máximo de procesos

for foo in `some-command` 
do 
    do-something $foo 
done 

do-something está obligado cpu y tengo una agradable procesador de núcleo brillante 4. Me gustaría poder ejecutar hasta 4 do-something de una vez.

El enfoque ingenuo parece ser:

for foo in `some-command` 
do 
    do-something $foo & 
done 

Esto ejecutará todosdo-something s de una vez, pero hay un par de inconvenientes, sobre todo que lo hacen, algo también pueden tener alguna significativa de E/S, que realizar todo de una vez puede disminuir la velocidad 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 se completen todos los do-something s.

¿Cómo se escribiría este ciclo para que siempre haya X do-something funcionando a la vez?

+2

Como sidenode, he soñado con la adición de la opción -j de hacer a la bas h para primitivo. No funcionaría siempre, pero para algunos casos simples en los que sabes que el cuerpo del ciclo va a hacer algo único para cada iteración, sería bastante sencillo decir "para -j 4 ...". – unwind

+1

Referencia cruzada a http://stackoverflow.com/questions/1537956/bash-limit-the-number-of-concurrent-jobs/1685440#1685440 para una solución bash que mitiga los problemas de rendimiento y permite grupos de subprocesos , mantenido separado. – paxdiablo

+1

Recomiendo mi solución http://stackoverflow.com/a/28965927/340581 – Tuttle

Respuesta

50

dependiendo de lo que quiere hacer xargs también puede ayudar (en este caso: la conversión de 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 

A partir 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. 
+8

Este método, en mi opinión, es la solución más elegante. Excepto que, como soy paranoico, siempre me gusta usar 'find [...] -print0' y' xargs -0'. – amphetamachine

+7

'cpus = $ (getconf _NPROCESSORS_ONLN)' –

+1

Del manual, ¿por qué no usar '--max-procs = 0' para obtener tantos procesos como sea posible? – EverythingRightPlace

10

En lugar de un bash simple, use un Makefile, luego especifique el número de trabajos simultáneos con make -jX donde X es el número de trabajos para ejecutar a la vez.

O puede utilizar wait ("man wait"): lanzar varios procesos secundarios, llame wait - sino que terminará cuando el niño procesa acabado.

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, simplemente verifica qué contiene la variable.

+1

Gracias por esto, aunque el código no está terminado, me ha dado la respuesta a un problema que estoy teniendo en el trabajo. – gerikson

8

¿Quizás intente una utilidad de paralelización en lugar de reescribir el bucle? Soy un gran fan de xjobs. Uso 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

22
 
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 ... 
+10

Darse cuenta de que hay algunos ** comillas graves ** aquí, por lo que cualquier trabajo que requiera espacios en los argumentos fallará gravemente; además, esta secuencia de comandos comerá su CPU con vida mientras espera que algunos trabajos finalicen si se solicitan más trabajos que lo que permite maxjobs. – lhunath

+1

También tenga en cuenta que esto supone que su secuencia de comandos no está haciendo nada más que hacer con los trabajos; si lo es, también contará esos hacia maxjobs. – lhunath

+1

Es posible que desee utilizar "jobs -pr" para limitar la ejecución de trabajos. – amphetamachine

2

El proyecto de trabajo en el utiliza esperar comando para el control de Shell en paralelo (en realidad ksh) procesos. Para abordar sus preocupaciones sobre IO, en un SO 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 golpear el hardware físico. Los otros procesos a menudo podrán recuperar el bloque de la memoria caché de disco del sistema operativo. Obviamente, leer de 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.

8

Aquí una solución alternativa que se puede insertar en .bashrc y se utiliza para todos los días un trazador de líneas:

function pwait() { 
    while [ $(jobs -p | wc -l) -ge $1 ]; do 
     sleep 1 
    done 
} 

Para usarlo, todo lo que uno tiene que hacer es poner & después de que los puestos de trabajo y una llamada pwait, la parámetro indica el número de procesos paralelos:

for i in *; do 
    do_something $i & 
    pwait 10 
done 

que sería mejor utilizar wait lugar de espera ocupada en la salida del jobs -p, pero no parece ser una solución obvia a esperar hasta cualquiera de los g Se terminaron algunos trabajos en lugar de todos.

6

Si bien hacer esto bien en bash es probablemente imposible, puede hacer un semi-derecha con bastante facilidad. bstark dio una buena aproximación de la derecha pero su cuenta con los siguientes defectos:

    división
  • Palabra: No se puede pasar cualquier puestos de trabajo a lo que utilizan cualquiera de los siguientes caracteres en sus argumentos: espacios, tabuladores, nuevas líneas, estrellas , signos de interrogación. Si lo haces, las cosas se romperán, posiblemente de forma inesperada.
  • Depende del resto de su script no copiar nada. Si lo hace, o más tarde agrega algo a la secuencia de comandos que se envía en segundo plano porque se olvidó de que no le permitieron usar trabajos con antecedentes 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 éste es fácilmente adaptable también de comprobar el código de salida de cada puesto de trabajo, ya que termina por lo que puede advertir al usuario si un trabajo falla o establece un código de salida para scheduleAll de acuerdo con la cantidad de trabajos que fallaron, o algo así.

El problema con este código es sólo eso:

  • It horarios cuatro (en este caso) puestos de trabajo a la vez y luego espera a que los cuatro hasta el final. Algunos se pueden hacer antes que otros, lo que hará que el siguiente lote de cuatro trabajos espere hasta que se complete el lote más largo del lote anterior.

Una solución que se ocupa de esta última cuestión tendría que utilizar kill -0 para sondear si alguno de los procesos han desaparecido en lugar de la wait y programar el trabajo siguiente. 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 que comprueba si ha finalizado. 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á que su trabajo ha terminado y las cosas se romperán nuevamente.

Una solución perfecta no es posible en bash.

5

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 tiene que ejecutar $ SOME_COMMAND en archivos * .input cada uno de los cuales produce * .output, puede utilizar el makefile

 
INPUT = a.input b.input 
OUTPUT = $(INPUT:.input=.output) 

%.output : %.input 
    $(SOME_COMMAND) $< [email protected] 

all: $(OUTPUT) 

y luego simplemente ejecutar

 
make -j<NUMBER> 

para funcionar como máximo NUMBER comandos en paralelo.

34

Con GNU paralelo http://www.gnu.org/software/parallel/ puede escribir:

some-command | parallel do-something 

paralelo GNU también es compatible con trabajos que se ejecutan en equipos remotos. Esto ejecutará uno por núcleo de CPU en los equipos remotos - incluso si tienen diferente número de núcleos:

some-command | parallel -S server1,server2 do-something 

Un ejemplo más avanzado: Aquí tenemos una lista de archivos que queremos my_script que se ejecuta. 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 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 sea procesado y transferido a la computadora dada. Cuando termina my_script, queremos foo.out transferido de nuevo y entonces queremos foo.jpeg y foo.out quitado del equipo remoto:

cat list_of_files | \ 
parallel --trc {.}.out -S server1,server2,: \ 
"my_script {} > {.}.out" 

paralelo GNU se asegura la salida de cada puesto de trabajo no se mezcla, por lo que puede utilizar la salida como entrada para otro programa:

some-command | parallel do-something | postprocess 

ver los vídeos para más ejemplos: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1

+1

Tenga en cuenta que esto es realmente útil cuando se usa un comando 'find' para generar una lista de archivos, porque no solo previene el problema cuando hay un espacio dentro de un nombre de archivo que aparece en' for i in ...; do' pero find también puede hacer 'find -name \ *. extension1 -or -name \ *. extension2' que GNU parallel's {.} puede manejar muy bien. –

+0

Plus 1 aunque el 'gato' es, por supuesto, [inútil.] (Http://www.iki.fi/era/unix/award.html) – tripleee

+0

@tripleee Re: Uso inútil de cat. Ver http://oletange.blogspot.dk/2013/10/useless-use-of-cat.html –

1

Esto podría ser lo suficientemente bueno para la mayoría de los propósitos, pero no es óptima.

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

Se puede utilizar un simple anidada para el bucle (enteros correspondientes sustitutos para N y M continuación):

for i in {1..N}; do 
    (for j in {1..M}; do do_something; done &); 
done 

Esto ejecutará hacer_algo veces N * M en rondas M, cada ronda de ejecutar trabajos N en paralelo. Puedes hacer que N sea igual al número de CPU que tienes.

3

función para la fiesta:

parallel() 
{ 
    awk "BEGIN{print \"all: ALL_TARGETS\\n\"}{print \"TARGET_\"NR\":\\n\\[email protected]\"\$0\"\\n\"}END{printf \"ALL_TARGETS:\";for(i=1;i<=NR;i++){printf \" TARGET_%d\",i};print\"\\n\"}" | make [email protected] -f - all 
} 

usando:

cat my_commands | parallel -j 4 
+0

El uso de 'make -j' es inteligente, pero sin ninguna explicación y ese blob de código Awk de solo escritura, me abstengo de votar en sentido ascendente. – tripleee

-1

$ DOMINIOS = "lista de algunos de dominio en los comandos" de foo en some-command hacer

eval `some-command for $DOMAINS` & 

    job[$i]=$! 

    i=$((i + 1)) 

hecho

Ndomains = echo $DOMAINS |wc -w

for i in $ (SEC 1 1 $ Ndomains) hago echo "esperar a $ {trabajo [$ i]}" espera "$ {trabajo [$ i]}" hecho

en este concepto funcionará para la paralelización. Lo importante es que la última línea de eval es '&' que pondrá los comandos en los fondos.

0

Aquí es cómo me las arreglé para 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 
0

Mi solución para mantener siempre un determinado número de procesos en ejecución, mantener el seguimiento de errores y manejar los procesos ubnterruptible/zombie:

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" 
Cuestiones relacionadas