usando paralelo para procesar archivos de entrada únicos a archivos de salida únicos

18

Tengo un problema de scripting de shell en el que recibo un directorio lleno de archivos de entrada (cada archivo contiene muchas líneas de entrada), y necesito procesarlos individualmente, redirigiendo cada una de sus salidas a un archivo único (también conocido como file_1.input necesita para ser capturado en file_1.output, y así sucesivamente).

Antes del paralelo , simplemente iteraría sobre cada archivo en el directorio y realizaría mi comando, mientras realizaba algún tipo de técnica de temporizador / conteo para no abrumar a los procesadores (suponiendo que cada proceso tuviera un tiempo de ejecución constante). Sin embargo, sé que ese no siempre será el caso, por lo que usar una solución similar a "paralela" parece ser la mejor manera de obtener múltiples secuencias de comandos de shell sin escribir código personalizado.

Si bien he pensado en algunas formas de acelerar en paralelo para procesar cada uno de estos archivos (y permitirme administrar mis núcleos de manera eficiente), todos parecen extravagantes. Tengo lo que creo que es un caso de uso bastante fácil, por lo que preferiría mantenerlo lo más limpio posible (y nada en los ejemplos paralelos parece ser mi problema).

¡Cualquier ayuda sería apreciada!

ejemplo de directorio de entrada:

> ls -l input_files/
total 13355
location1.txt
location2.txt
location3.txt
location4.txt
location5.txt

Guión:

> cat proces_script.sh
#!/bin/sh

customScript -c 33 -I -file [inputFile] -a -v 55 > [outputFile]

Actualización : Después de leer la respuesta de Ole a continuación, pude juntar las piezas que faltaban para mi propia implementación paralela. Si bien su respuesta es excelente, aquí está mi investigación adicional y las notas que tomé:

En lugar de ejecutar mi proceso completo, pensé en comenzar con un comando de prueba de concepto para probar su solución en mi entorno. Vea mis dos implementaciones diferentes (y notas):

find /home/me/input_files -type f -name *.txt | parallel cat /home/me/input_files/{} '>' /home/me/output_files/{.}.out

Utiliza find (no ls, que puede causar problemas) para encontrar todos los archivos aplicables dentro de mi directorio de archivos de entrada y luego redirige sus contenidos a un directorio y archivo por separado. Mi problema desde arriba fue leer y redirigir (el guión real era simple), por lo que reemplazar el guión con gato fue una buena prueba de concepto.

parallel cat '>' /home/me/output_files/{.}.out :::  /home/me/input_files/*

Esta segunda solución utiliza el paradigma variable de entrada de paralelo para leer los archivos, sin embargo, para un novato, esto era mucho más confuso. Para mí, usar find a and pipe satisfizo mis necesidades perfectamente.

J Jones
fuente

Respuestas:

27

GNU Parallel está diseñado para este tipo de tareas:

parallel customScript -c 33 -I -file {} -a -v 55 '>' {.}.output ::: *.input

o:

ls | parallel customScript -c 33 -I -file {} -a -v 55 '>' {.}.output

Ejecutará un trabajo por núcleo de CPU.

Puede instalar GNU Parallel simplemente por:

wget https://git.savannah.gnu.org/cgit/parallel.git/plain/src/parallel
chmod 755 parallel
cp parallel sem

Mire los videos de introducción de GNU Parallel para obtener más información: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1

Ole Tange
fuente
Gran respuesta (y puntos importantes para leer mi solicitud de usar paralelo).
J Jones
5

La forma estándar de hacer esto es configurar una cola y generar cualquier cantidad de trabajadores que sepan cómo extraer algo de la cola y procesarla. Puede usar un fifo (también conocido como canalización) para la comunicación entre estos procesos.

A continuación se muestra un ejemplo ingenuo para demostrar el concepto.

Un script de cola simple:

#!/bin/sh
mkfifo /tmp/location-queue
for i in inputfiles/*; do
  echo $i > /tmp/location-queue
done
rm /tmp/location-queue

Y un trabajador:

#!/bin/sh
while read file < /tmp/location-queue; do
  process_file "$file"
done

process_file podría definirse en algún lugar de su trabajador, y puede hacer lo que sea que necesite.

Una vez que tenga esas dos piezas, puede tener un monitor simple que inicie el proceso de la cola y cualquier número de procesos de trabajo.

Supervisar script:

#!/bin/sh
queue.sh &
num_workers="$1"
i=0
while [ $i < $num_workers ]; do
  worker.sh &
  echo $! >> /tmp/worker.pids
  i=$((i+1))
done
monitor_workers

Ahí tienes. Si realmente hace esto, es mejor configurar el fifo en el monitor y pasar la ruta tanto a la cola como a los trabajadores, para que no estén acoplados ni pegados a una ubicación específica para el fifo. Lo configuré de esta manera en la respuesta específicamente para que quede claro lo que estás usando mientras lo lees.

Shawn J. Goff
fuente
¿Cómo es el monitor lo suficientemente inteligente como para pausar el desove de los nuevos trabajadores hasta que termine el siguiente (es decir, ¿dónde se decrementa $ i)? ---- Respondiendo a mi propia edición, los trabajadores nunca desaparecen, solo procesan archivos hasta que todo el procesamiento se haya agotado (de ahí el ciclo while dentro de los 'procesadores' también).
J Jones
¿Cuál es la línea "monitor_workers" al final del script del monitor?
J Jones
@JJones, monitor_workerses como process_file, es una función que hace lo que quieras. Sobre el monitor, tenías razón; debe guardar las imágenes de sus trabajadores (para que pueda enviar una señal de muerte) y el contador debe incrementarse cuando se inicia un trabajador. He editado la respuesta para incluir eso.
Shawn J. Goff
Realmente aprecio tu trabajo, pero creo que deberías usar GNU's parallel. Creo que es tu idea, totalmente implementada.
motobói
5

Otro ejemplo:

ls *.txt | parallel 'sort {} > {.}.sorted.txt'

Encontré los otros ejemplos innecesariamente complejos, cuando en la mayoría de los casos, lo anterior es lo que puede haber estado buscando.

caviar desacelerado
fuente
4

Una herramienta comúnmente disponible que puede hacer paralelización es make. GNU make y algunos otros tienen la -jopción de realizar compilaciones paralelas.

.SUFFIXES: .input .output
.input.output:
        process_one_file <$< >[email protected]
        mv -f [email protected] $@

Ejecutar makeasí (supongo que sus nombres de archivo no contienen caracteres especiales, makeno es bueno con ellos):

make -j 4 $(for x in *.input; do echo ${x%.*}.output; done)
Gilles 'SO- deja de ser malvado'
fuente
En mi opinión, esta es la solución más inteligente :)
h4unt3r
3

Esto es para realizar el mismo comando en un gran conjunto de archivos en el directorio actual:

#!/bin/sh
trap 'worker=`expr $worker - 1`' USR1  # free up a worker
worker=0  # current worker
num_workers=10  # maximum number of workers
for file in *.txt; do
    if [ $worker -lt $num_workers ]; then
        {   customScript -c 33 -I -file $file -a -v 55 > `basename $file .txt`.outtxt 
            kill -USR1 $$ 2>/dev/null  # signal parent that we're free
        } &
        echo $worker/$num_worker $! $file  # feedback to caller
        worker=`expr $worker + 1`
    else
        wait # for a worker to finish
    fi
done

Esto ejecuta el customScripten cada txtarchivo, poniendo la salida en outtxtarchivos. Cambia según lo necesites. La clave para que esto funcione es el procesamiento de la señal, utilizando SIGUSR1 para que el proceso secundario pueda informar al proceso principal que se ha completado. El uso de SIGCHLD no funcionará, ya que la mayoría de las declaraciones en el script generarán señales de SIGCHLD para el script de shell. Intenté esto reemplazando su comando con sleep 1, el programa usó 0.28s de la CPU del usuario y 0.14s de la CPU del sistema; esto fue solo en unos 400 archivos.

Arcege
fuente
¿Cómo es la 'espera' lo suficientemente inteligente como para tomar el mismo archivo que se está iterando actualmente y volver a ingresar la declaración hermana "if"?
J Jones
No es lo waitsuficientemente "inteligente"; pero volverá después de recibir la SIGUSR1señal. El niño / trabajador envía un mensaje SIGUSR1al padre, que se captura ( trap), y disminuye $worker( trapcláusula) y regresa de forma anormal wait, permitiendo que la if [ $worker -lt $num_workers ]cláusula se ejecute.
Arcege
0

O simplemente use xargs -P, no es necesario instalar un software adicional:

find . -type f -print0 | xargs -0 -I'XXX' -P4 -n1 custom_script -input "XXX" -output "XXX.out"

Un poco de explicación para las opciones:

  • -I'XXX' establece la cadena que se reemplazará en la plantilla de comando con el nombre del archivo
  • -P4 ejecutará 4 procesos en paralelo
  • -n1 colocará solo un archivo por ejecución aunque se encuentren dos XXX
  • -print0y -0trabajar juntos, permitiéndole tener caracteres especiales (como espacios en blanco) en los nombres de archivo
Piotr Czapla
fuente