Paralelo a un Bash FOR Loop


109

He estado tratando de paralelizar el siguiente script, específicamente cada una de las tres instancias de bucle FOR, usando GNU Parallel, pero no he podido. Los 4 comandos contenidos dentro del ciclo FOR se ejecutan en serie, cada ciclo toma alrededor de 10 minutos.

#!/bin/bash

kar='KAR5'
runList='run2 run3 run4'
mkdir normFunc
for run in $runList
do 
  fsl5.0-flirt -in $kar"deformed.nii.gz" -ref normtemp.nii.gz -omat $run".norm1.mat" -bins 256 -cost corratio -searchrx -90 90 -searchry -90 90 -searchrz -90 90 -dof 12 
  fsl5.0-flirt -in $run".poststats.nii.gz" -ref $kar"deformed.nii.gz" -omat $run".norm2.mat" -bins 256 -cost corratio -searchrx -90 90 -searchry -90 90 -searchrz -90 90 -dof 12 
  fsl5.0-convert_xfm -concat $run".norm1.mat" -omat $run".norm.mat" $run".norm2.mat"
  fsl5.0-flirt -in $run".poststats.nii.gz" -ref normtemp.nii.gz -out $PWD/normFunc/$run".norm.nii.gz" -applyxfm -init $run".norm.mat" -interp trilinear

  rm -f *.mat
done

Respuestas:


94

¿Por qué no los bifurcas (también conocido como fondo)?

foo () {
    local run=$1
    fsl5.0-flirt -in $kar"deformed.nii.gz" -ref normtemp.nii.gz -omat $run".norm1.mat" -bins 256 -cost corratio -searchrx -90 90 -searchry -90 90 -searchrz -90 90 -dof 12 
    fsl5.0-flirt -in $run".poststats.nii.gz" -ref $kar"deformed.nii.gz" -omat $run".norm2.mat" -bins 256 -cost corratio -searchrx -90 90 -searchry -90 90 -searchrz -90 90 -dof 12 
    fsl5.0-convert_xfm -concat $run".norm1.mat" -omat $run".norm.mat" $run".norm2.mat"
    fsl5.0-flirt -in $run".poststats.nii.gz" -ref normtemp.nii.gz -out $PWD/normFunc/$run".norm.nii.gz" -applyxfm -init $run".norm.mat" -interp trilinear
}

for run in $runList; do foo "$run" & done

En caso de que no esté claro, la parte importante está aquí:

for run in $runList; do foo "$run" & done
                                   ^

Causando que la función se ejecute en un shell bifurcado en segundo plano. Eso es paralelo


66
Eso funcionó a las mil maravillas. Gracias. Una implementación tan simple (¡me hace sentir tan estúpido ahora!).
Ravnoor S Gill

8
En caso de que tuviera 8 archivos para ejecutar en paralelo pero solo 4 núcleos, ¿podría integrarse en tal configuración o requeriría un Programador de trabajos?
Ravnoor S Gill

66
Realmente no importa en este contexto; Es normal que el sistema tenga procesos más activos que núcleos. Si tiene muchas tareas cortas , lo ideal sería alimentar una cola atendida por un número o hilos de trabajo <el número de núcleos. No sé con qué frecuencia eso se hace realmente con scripts de shell (en cuyo caso, no serían hilos, serían procesos independientes), pero con relativamente pocas tareas largas sería inútil. El programador del sistema operativo se encargará de ellos.
Ricitos de oro

17
También es posible que desee agregar un waitcomando al final para que el script maestro no salga hasta que lo hagan todos los trabajos en segundo plano.
psusi

1
También estaría bien limitar el número de procesos concurrentes: cada uno de mis procesos usa el 100% del tiempo del núcleo durante unos 25 minutos. Esto está en un servidor compartido con 16 núcleos, donde muchas personas ejecutan trabajos. Necesito ejecutar 23 copias del script. Si los ejecuto todos al mismo tiempo, entonces inundaré el servidor y lo dejaré inútil para todos los demás durante una hora o dos (la carga sube a 30, todo lo demás se ralentiza). Supongo que podría hacerse nice, pero entonces no sé si alguna vez terminaría ...
nada101

150

Tarea de muestra

task(){
   sleep 0.5; echo "$1";
}

Carreras secuenciales

for thing in a b c d e f g; do 
   task "$thing"
done

Carreras paralelas

for thing in a b c d e f g; do 
  task "$thing" &
done

Ejecuciones paralelas en lotes de proceso N

N=4
(
for thing in a b c d e f g; do 
   ((i=i%N)); ((i++==0)) && wait
   task "$thing" & 
done
)

También es posible usar FIFOs como semáforos y usarlos para garantizar que se generen nuevos procesos lo antes posible y que no se ejecuten más de N procesos al mismo tiempo. Pero requiere más código.

N procesos con un semáforo basado en FIFO:

open_sem(){
    mkfifo pipe-$$
    exec 3<>pipe-$$
    rm pipe-$$
    local i=$1
    for((;i>0;i--)); do
        printf %s 000 >&3
    done
}
run_with_lock(){
    local x
    read -u 3 -n 3 x && ((0==x)) || exit $x
    (
     ( "$@"; )
    printf '%.3d' $? >&3
    )&
}

N=4
open_sem $N
for thing in {a..g}; do
    run_with_lock task $thing
done 

44
La línea waitque contiene básicamente permite que todos los procesos se ejecuten, hasta que llegue al nthproceso, luego espera a que todos los demás terminen de ejecutarse, ¿no es así?
naught101

Si ies cero, llame a wait. Incremento idespués de la prueba cero.
PSkocik

2
@ naught101 Sí. waitw / no arg espera a todos los niños. Eso lo hace un poco despilfarrador. El enfoque de semáforo basado en tubería le brinda una concurrencia más fluida (lo he estado usando en un sistema de construcción basado en shell personalizado junto con -nt/ -otcomprobaciones con éxito durante un tiempo ahora)
PSkocik

1
@ BeowulfNode42 No tiene que salir. El estado de retorno de la tarea no dañará la consistencia del semáforo siempre que el estado (o algo con esa longitud de byte) se vuelva a escribir en el quince después de que el proceso de la tarea salga / se bloquee.
PSkocik

1
Para su información, el mkfifo pipe-$$comando necesita acceso de escritura adecuado al directorio actual. Por lo tanto, prefiero especificar la ruta completa, ya /tmp/pipe-$$que lo más probable es que tenga acceso de escritura disponible para el usuario actual en lugar de depender del directorio actual. Sí, reemplace las 3 ocurrencias de pipe-$$.
BeowulfNode42

65
for stuff in things
do
( something
  with
  stuff ) &
done
wait # for all the something with stuff

Si realmente funciona depende de sus comandos; No estoy familiarizado con ellos. El rm *.matparece un poco propenso a conflictos si se ejecuta en paralelo ...


2
Esto funciona perfectamente también. Tienes razón, tendría que cambiar rm *.mata algo como rm $run".mat"hacer que funcione sin que un proceso interfiera con el otro. Gracias .
Ravnoor S Gill

@RavnoorSGill ¡Bienvenido a Stack Exchange! Si esta respuesta resolvió su problema, márquelo como aceptado marcando la marca de verificación que se encuentra al lado.
Gilles

77
+1 para wait, que olvidé.
Ricitos de oro

55
Si hay toneladas de "cosas", ¿esto no iniciará toneladas de procesos? Sería mejor comenzar solo un número razonable de procesos simultáneamente, ¿verdad?
David Doria

1
Consejo muy útil! ¿Cómo configurar el número de hilos en este caso?
Dadong Zhang

30
for stuff in things
do
sem -j+0 ( something
  with
  stuff )
done
sem --wait

Esto usará semáforos, paralelizando tantas iteraciones como el número de núcleos disponibles (-j +0 significa que paralelizará N + 0 trabajos , donde N es el número de núcleos disponibles ).

sem --wait le dice que espere hasta que todas las iteraciones en el ciclo for hayan terminado la ejecución antes de ejecutar las líneas de código sucesivas.

Nota: necesitará "paralelo" del proyecto paralelo GNU (sudo apt-get install parallel).


1
¿es posible pasar de 60? el mío arroja un error que dice que no hay suficientes descriptores de archivo.
chovy

Si esto arroja un error de sintaxis debido a las llaves para alguien también, mire la respuesta de moritzschaefer.
Nicolai

10

Una forma realmente fácil que uso a menudo:

cat "args" | xargs -P $NUM_PARALLEL command

Esto ejecutará el comando, pasando cada línea del archivo "args", en paralelo, ejecutando como máximo $ NUM_PARALLEL al mismo tiempo.

También puede buscar en la opción -I para xargs, si necesita sustituir los argumentos de entrada en diferentes lugares.


6

Parece que los trabajos fsl dependen unos de otros, por lo que los 4 trabajos no se pueden ejecutar en paralelo. Las ejecuciones, sin embargo, pueden ejecutarse en paralelo.

Haga una función bash ejecutando una sola ejecución y ejecute esa función en paralelo:

#!/bin/bash

myfunc() {
    run=$1
    kar='KAR5'
    mkdir normFunc
    fsl5.0-flirt -in $kar"deformed.nii.gz" -ref normtemp.nii.gz -omat $run".norm1.mat" -bins 256 -cost corratio -searchrx -90 90 -searchry -90 90 -searchrz -90 90 -dof 12 
    fsl5.0-flirt -in $run".poststats.nii.gz" -ref $kar"deformed.nii.gz" -omat $run".norm2.mat" -bins 256 -cost corratio -searchrx -90 90 -searchry -90 90 -searchrz -90 90 -dof 12 
    fsl5.0-convert_xfm -concat $run".norm1.mat" -omat $run".norm.mat" $run".norm2.mat"
    fsl5.0-flirt -in $run".poststats.nii.gz" -ref normtemp.nii.gz -out $PWD/normFunc/$run".norm.nii.gz" -applyxfm -init $run".norm.mat" -interp trilinear
}

export -f myfunc
parallel myfunc ::: run2 run3 run4

Para obtener más información, vea los videos de introducción: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1 y pase una hora caminando a través del tutorial http://www.gnu.org/software/parallel/parallel_tutorial.html Su comando Line te amará por eso.


Si está utilizando un shell que no sea bash, también deberá hacerlo export SHELL=/bin/bashantes de ejecutarlo en paralelo. De lo contrario, obtendrá un error como:Unknown command 'myfunc arg'
AndrewHarvey

1
@ AndrewHarvey: ¿no es eso para lo que es el shebang?
naught101

5

Ejecución paralela en proceso N máximo concurrente

#!/bin/bash

N=4

for i in {a..z}; do
    (
        # .. do your stuff here
        echo "starting task $i.."
        sleep $(( (RANDOM % 3) + 1))
    ) &

    # allow only to execute $N jobs in parallel
    if [[ $(jobs -r -p | wc -l) -gt $N ]]; then
        # wait only for first job
        wait -n
    fi

done

# wait for pending jobs
wait

echo "all done"

3

Realmente me gusta la respuesta de @lev, ya que proporciona control sobre el número máximo de procesos de una manera muy simple. Sin embargo, como se describe en el manual , sem no funciona con corchetes.

for stuff in things
do
sem -j +0 "something; \
  with; \
  stuff"
done
sem --wait

Hace el trabajo.

-j + N Agregar N a la cantidad de núcleos de CPU. Ejecute hasta este número de trabajos en paralelo. Para trabajos intensivos en cómputo, -j +0 es útil ya que ejecutará simultáneamente trabajos de número de núcleos de CPU.

-j -N Resta N del número de núcleos de CPU. Ejecute hasta este número de trabajos en paralelo. Si el número evaluado es menor que 1, se utilizará 1. Ver también --use-cpus-lugar-de-núcleos.


1

En mi caso, no puedo usar el semáforo (estoy en git-bash en Windows), así que se me ocurrió una forma genérica de dividir la tarea entre los trabajadores de N, antes de que comiencen.

Funciona bien si las tareas toman aproximadamente la misma cantidad de tiempo. La desventaja es que, si uno de los trabajadores toma mucho tiempo para hacer su parte del trabajo, los otros que ya terminaron no ayudarán.

División del trabajo entre N trabajadores (1 por núcleo)

# array of assets, assuming at least 1 item exists
listAssets=( {a..z} ) # example: a b c d .. z
# listAssets=( ~/"path with spaces/"*.txt ) # could be file paths

# replace with your task
task() { # $1 = idWorker, $2 = asset
  echo "Worker $1: Asset '$2' START!"
  # simulating a task that randomly takes 3-6 seconds
  sleep $(( ($RANDOM % 4) + 3 ))
  echo "    Worker $1: Asset '$2' OK!"
}

nVirtualCores=$(nproc --all)
nWorkers=$(( $nVirtualCores * 1 )) # I want 1 process per core

worker() { # $1 = idWorker
  echo "Worker $1 GO!"
  idAsset=0
  for asset in "${listAssets[@]}"; do
    # split assets among workers (using modulo); each worker will go through
    # the list and select the asset only if it belongs to that worker
    (( idAsset % nWorkers == $1 )) && task $1 "$asset"
    (( idAsset++ ))
  done
  echo "    Worker $1 ALL DONE!"
}

for (( idWorker=0; idWorker<nWorkers; idWorker++ )); do
  # start workers in parallel, use 1 process for each
  worker $idWorker &
done
wait # until all workers are done

0

Tuve problemas con @PSkocikla solución. Mi sistema no tiene GNU Parallel disponible como paquete y semarrojó una excepción cuando lo construí y lo ejecuté manualmente. Luego probé el ejemplo del semáforo FIFO, que también arrojó algunos otros errores con respecto a la comunicación.

@eyeApps sugerí xargs pero no sabía cómo hacerlo funcionar con mi caso de uso complejo (los ejemplos serían bienvenidos).

Aquí está mi solución para trabajos paralelos que procesan hasta Ntrabajos a la vez según lo configurado por _jobs_set_max_parallel:

_lib_jobs.sh:

function _jobs_get_count_e {
   jobs -r | wc -l | tr -d " "
}

function _jobs_set_max_parallel {
   g_jobs_max_jobs=$1
}

function _jobs_get_max_parallel_e {
   [[ $g_jobs_max_jobs ]] && {
      echo $g_jobs_max_jobs

      echo 0
   }

   echo 1
}

function _jobs_is_parallel_available_r() {
   (( $(_jobs_get_count_e) < $g_jobs_max_jobs )) &&
      return 0

   return 1
}

function _jobs_wait_parallel() {
   # Sleep between available jobs
   while true; do
      _jobs_is_parallel_available_r &&
         break

      sleep 0.1s
   done
}

function _jobs_wait() {
   wait
}

Ejemplo de uso:

#!/bin/bash

source "_lib_jobs.sh"

_jobs_set_max_parallel 3

# Run 10 jobs in parallel with varying amounts of work
for a in {1..10}; do
   _jobs_wait_parallel

   # Sleep between 1-2 seconds to simulate busy work
   sleep_delay=$(echo "scale=1; $(shuf -i 10-20 -n 1)/10" | bc -l)

   ( ### ASYNC
   echo $a
   sleep ${sleep_delay}s
   ) &
done

# Visualize jobs
while true; do
   n_jobs=$(_jobs_get_count_e)

   [[ $n_jobs = 0 ]] &&
      break

   sleep 0.1s
done
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.