Paralelamente el script Bash con la cantidad máxima de procesos

Digamos que tengo un bucle en Bash:

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

do-something está atado a la CPU y tengo un bonito y shiny procesador de 4 núcleos. Me gustaría poder ejecutar hasta 4 do-something para do-something a la vez.

El enfoque ingenuo parece ser:

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

Esto ejecutará todos los “ do-something a la vez, pero hay un par de inconvenientes, principalmente que “do-something” también puede tener algunas E / S significativas, que al realizarlas 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 se completan todas las do-something .

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

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. 

Con GNU Parallel http://www.gnu.org/software/parallel/ puedes escribir:

 some-command | parallel do-something 

GNU Parallel también admite ejecutar trabajos en computadoras remotas. Esto ejecutará uno por núcleo de CPU en las computadoras remotas, incluso si tienen diferente número de núcleos:

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

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 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 finalice my_script, queremos que foo.out se transfiera nuevamente y luego queremos que foo.jpeg y foo.out sean eliminados 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 el resultado como entrada para otro progtwig:

 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];  hacer
                 jobcnt = (`trabajos -p`)
                 if [$ {# jobcnt [@]} -lt $ maxjobs];  entonces
                         hacer algo $ 1 y
                         cambio  
                 más
                         dormir 1
                 fi
         hecho
         Espere
 }

 paralelizar arg1 arg2 "5 args a tercer trabajo" 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 la cantidad de trabajos que se ejecutarán a la vez.

O puede usar wait (” man wait “): inicie varios procesos secundarios, llame a wait : saldrá cuando termine el proceso hijo.

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

Tal vez 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

Aquí hay una solución alternativa que puede insertarse en .bashrc y usarse para un trazador de líneas diario:

 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 los trabajos y una llamada pwait, el parámetro da la cantidad de procesos paralelos:

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

Sería mejor usar wait lugar de busy esperando en la salida de jobs -p , pero no parece haber una solución obvia para esperar hasta que se termine cualquiera de los trabajos dados en lugar de todos.

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

  • División de palabras: no puede pasarle ningún trabajo que use ninguno de los siguientes caracteres en sus argumentos: espacios, tabs, líneas nuevas, estrellas, signos de interrogación. Si lo haces, las cosas se romperán, posiblemente de forma inesperada.
  • Se basa en el rest de su secuencia de comandos para no hacer un fondo de 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 este es fácilmente adaptable para verificar también el código de salida de cada trabajo, ya que puede avisar al usuario si falla un trabajo o establecer un código de salida para scheduleAll acuerdo con la cantidad de trabajos que fallaron, o algo así.

El problema con este código es solo eso:

  • Progtwig cuatro trabajos (en este caso) a la vez y luego espera que finalicen los cuatro. 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 este último problema debería usar kill -0 para sondear si alguno de los procesos ha desaparecido en lugar de wait y progtwigr el próximo 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 comprueba si ha finalizado. Si el trabajo finaliza 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, kill -0 no notará que su trabajo ha terminado y las cosas se romperán de nuevo. .

Una solución perfecta no es posible en bash .

Si está familiarizado con el comando make , la mayoría de las veces puede express 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
 SALIDA = $ (ENTRADA: .input = .output)

 %.salida entrada
     $ (SOME_COMMAND) $ <$ @

 todo: $ (SALIDA)

y luego solo corre

 hacer -j 

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 } 

utilizando:

 cat my_commands | parallel -j 4 

El proyecto en el que trabajo utiliza el comando de espera para controlar procesos de shell paralelo (ksh en realidad). 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 encoding.

Esto podría 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 

Puede usar un bucle for nested simple (sustituya los enteros apropiados para 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_algo N * M veces en M rondas, cada ronda ejecuta N trabajos en paralelo. Puedes hacer que N sea igual al número 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 un número determinado de procesos en ejecución, seguir el seguimiento de errores y manejar 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" 

$ DOMAINS = “lista de algunos dominios en comandos” para foo en some-command do

 eval `some-command for $DOMAINS` & job[$i]=$! i=$(( i + 1)) 

hecho

Ndomains = echo $DOMAINS |wc -w

para i en $ (seq 1 1 $ Ndomains) echo eco “espera $ {job [$ i]}” wait “$ {job [$ i]}” done

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