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-somethingEs de inmediato.

El enfoque ingenuo parece ser:

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

Esto se ejecutará todo do-somethings 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-somethingEstamos terminados.

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

¿Fue útil?

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

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.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top