Paralelizar el script Bash con el número máximo de procesos

86

Digamos que tengo un bucle en Bash:

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

do-somethingestá vinculado a la CPU y tengo un procesador de 4 núcleos brillante y agradable. Me gustaría poder ejecutar hasta 4 do-somethinga 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-somethingcorreos electrónicos están terminados.

¿Cómo escribirías este bucle para que siempre haya X do-somethingcorriendo a la vez?

thelsdj
fuente
2
Como nodo lateral, he soñado con agregar la opción -j de make al bash para primitivo. No siempre funcionaría, pero para algunos casos simples en los que se sabe que el cuerpo del bucle hará algo único para cada iteración, sería bastante limpio decir "para -j 4 ...".
relajarse
1
Haga una referencia cruzada a stackoverflow.com/questions/1537956/… para obtener una solución bash que mitiga los problemas de rendimiento y permite grupos de subprocesos, que se mantienen separados.
paxdiablo
1
Recomendaría mi solución stackoverflow.com/a/28965927/340581
Tuttle

Respuestas:

62

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.
Fritz G. Mehner
fuente
9
Este método, en mi opinión, es la solución más elegante. Excepto que, como soy paranoico, siempre me gusta usar find [...] -print0y xargs -0.
amphetamachine
7
cpus=$(getconf _NPROCESSORS_ONLN)
mr.spuratic
1
Desde el manual, ¿por qué no utilizar --max-procs=0para obtener tantos procesos como sea posible?
EverythingRightPlace
@EverythingRightPlace, la pregunta pide explícitamente no más procesos que los procesadores disponibles. --max-procs=0es más como el intento del interrogador (iniciar tantos procesos como argumentos).
Toby Speight
39

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

some-command | parallel do-something

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:

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 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:

some-command | parallel do-something | postprocess

Vea los videos para ver más ejemplos: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1

Ole Tange
fuente
1
Tenga en cuenta que esto es realmente útil cuando se usa un findcomando 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 ...; dosino que find también puede hacer lo find -name \*.extension1 -or -name \*.extension2que el paralelo de GNU {.} Puede manejar muy bien.
Leo Izen
Más 1, aunque cates, por supuesto, inútil.
tripleee
@tripleee Re: Uso inútil de cat. Ver oletange.blogspot.dk/2013/10/useless-use-of-cat.html
Ole Tange
¡Oh, eres tú! Por cierto, ¿podrías actualizar el enlace de ese blog? La ubicación de partmaps.org está lamentablemente muerta, pero el redirector Iki debería continuar funcionando.
tripleee
22
maxjobs = 4
paralelizar () {
        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 argumentos al tercer trabajo" arg4 ...
bstark
fuente
10
Se dan cuenta de que hay algún grave underquoting pasando aquí por lo que cualquier trabajos que requieren espacios en los argumentos fallarán mal; Además, este script se comerá su CPU viva mientras espera que terminen algunos trabajos si se solicitan más trabajos de los que permite maxjobs.
lhunath
1
También tenga en cuenta que esto asume que su secuencia de comandos no está haciendo nada más que ver con trabajos; si es así, también los contará para maxjobs.
lhunath
1
Es posible que desee utilizar "jobs -pr" para limitar los trabajos en ejecución.
amphetamachine
1
Se agregó un comando de suspensión para evitar que el ciclo while se repita sin interrupción, mientras espera a que finalicen los comandos de hacer algo que ya se están ejecutando. De lo contrario, este bucle esencialmente ocuparía uno de los núcleos de la CPU. Esto también aborda la preocupación de @lhunath.
euphoria83
12

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 waitlugar de estar ocupado esperando la salida de jobs -p, pero no parece haber una solución obvia para esperar hasta que se termine cualquiera de los trabajos en lugar de todos.

Grumbel
fuente
11

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

O puede usar wait(" man wait"): lanzar varios procesos secundarios, llamar wait- 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 waitcomprobar qué contiene la variable.

skolima
fuente
1
Gracias por esto, aunque el código no está terminado, me ha dado la respuesta a un problema que tengo en el trabajo.
gerikson
el único problema es que si mata el script de primer plano (el que tiene el bucle), los trabajos que se estaban ejecutando no se eliminarán juntos
Girardi
8

¿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

tessein
fuente
7

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 make

INPUT = a.input b.input
SALIDA = $ (ENTRADA: .entrada = .salida)

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

todos: $ (SALIDA)

y luego solo corre

make -j <NUMBER>

para ejecutar como máximo NUMBER comandos en paralelo.

Idelic
fuente
6

Si bien hacer esto correctamente bashes probablemente imposible, puede hacer un semi-derecho con bastante facilidad. bstarkdio una aproximación justa del derecho, pero el suyo tiene los siguientes defectos:

  • División de palabras: no puede pasarle ningún trabajo que utilice alguno 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.
  • Se basa en el resto de su secuencia de comandos para no poner en segundo plano nada. Si lo hace, o luego agrega algo al script que se envía en segundo plano porque olvidó que no se le permitió 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 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 scheduleAllacuerdo con la cantidad de trabajos que fallaron, o algo así.

El problema con este código es solo eso:

  • Programa cuatro (en este caso) trabajos a la vez y luego espera a que terminen los cuatro. Algunos pueden realizarse antes que otros, lo que provocará que el siguiente lote de cuatro trabajos espere hasta que finalice el más largo del lote anterior.

Una solución que se encargue de este último problema tendría que utilizar kill -0para sondear si alguno de los procesos ha desaparecido en lugar del waity 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 la kill -0verificació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 -0no notará que su trabajo ha terminado y las cosas se romperán nuevamente.

No es posible una solución perfecta en bash.

lhunath
fuente
3

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
ilnar
fuente
El uso de make -jes inteligente, pero sin explicación y con esa cantidad de código Awk de solo escritura, me abstengo de votar a favor.
tripleee
2

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.

Jon Ericson
fuente
1

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
gato
fuente
1

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
Fernando
fuente
1

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:

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 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:

1 56871
2 56872
Skrat
fuente
0

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.

Adam Zalcman
fuente
0

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"
Orsiris de Jong
fuente
-1

$ DOMAINS = "lista de algunos dominios en comandos" para foo in 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 "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.

Jack
fuente