usando paralelo para processar arquivos de entrada exclusivos para arquivos de saída exclusivos

18

Eu tenho um problema de script de shell no qual recebo um diretório cheio de arquivos de entrada (cada arquivo contém muitas linhas de entrada) e preciso processá-los individualmente, redirecionando cada uma de suas saídas para um arquivo exclusivo (também conhecido como file_1.input para ser capturado em file_1.output e assim por diante).

Antes do paralelo , eu simplesmente iterava sobre cada arquivo no diretório e executava meu comando, enquanto fazia algum tipo de técnica de contagem / timer para não sobrecarregar os processadores (assumindo que cada processo tivesse um tempo de execução constante). No entanto, eu sei que nem sempre será o caso, portanto, usar uma solução "paralela" parece a melhor maneira de obter multi-threading de script de shell sem escrever código personalizado.

Embora tenha pensado em algumas maneiras de criar paralelos para processar cada um desses arquivos (e me permitir gerenciar meus núcleos com eficiência), todos eles parecem hacky. Eu tenho o que eu acho que é um caso de uso bastante fácil, por isso preferiria mantê-lo o mais limpo possível (e nada nos exemplos paralelos parece pular como sendo o meu problema.

Qualquer ajuda seria apreciada!

exemplo de diretório de entrada:

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

Roteiro:

> cat proces_script.sh
#!/bin/sh

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

Atualização : Depois de ler a resposta de Ole abaixo, pude reunir as peças que faltavam para minha própria implementação paralela. Embora sua resposta seja ótima, aqui está minha pesquisa adicional e as anotações que fiz:

Em vez de executar todo o meu processo, imaginei começar com um comando de prova de conceito para provar sua solução no meu ambiente. Veja minhas duas implementações diferentes (e notas):

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

Utiliza find (não ls, que pode causar problemas) para encontrar todos os arquivos aplicáveis ​​no diretório de arquivos de entrada e, em seguida, redireciona seu conteúdo para um diretório e arquivo separados. Meu problema acima foi a leitura e o redirecionamento (o script real era simples), portanto, substituir o script por cat foi uma boa prova de conceito.

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

Esta segunda solução usa o paradigma da variável de entrada paralela para ler os arquivos; no entanto, para um iniciante, isso era muito mais confuso. Para mim, usar find a and pipe atendeu minhas necessidades.

J Jones
fonte

Respostas:

27

O GNU Parallel foi projetado para este tipo de tarefas:

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

ou:

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

Ele executará um trabalho por núcleo de CPU.

Você pode instalar o GNU Parallel simplesmente:

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

Assista aos vídeos de introdução do GNU Parallel para saber mais: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1

Ole Tange
fonte
Ótima resposta (e pontos principais para a leitura do meu pedido de uso paralelo).
J Jones
5

A maneira padrão de fazer isso é configurar uma fila e gerar qualquer número de trabalhadores que sabem como extrair algo da fila e processá-lo. Você pode usar um fifo (também conhecido como pipe nomeado) para comunicação entre esses processos.

Abaixo está um exemplo ingênuo para demonstrar o conceito.

Um script de fila simples:

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

E um trabalhador:

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

process_file pode ser definido em algum lugar do seu trabalhador e pode fazer o que você precisar.

Depois de ter essas duas partes, é possível ter um monitor simples que inicie o processo da fila e qualquer número de processos de trabalho.

Script de monitor:

#!/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

Aí está. Se você realmente fizer isso, é melhor configurar o fifo no monitor e passar o caminho para a fila e os trabalhadores, para que eles não sejam acoplados e não estejam presos a um local específico para o fifo. Eu o configurei dessa maneira na resposta especificamente, para que fique claro o que você está usando ao lê-lo.

Shawn J. Goff
fonte
Como o monitor é inteligente o suficiente para interromper a geração de novos funcionários até que o próximo finalize (ou seja, onde $ i é diminuído)? ---- Respondendo à minha própria edição, os trabalhadores nunca vão embora, eles apenas processam arquivos até que todo o processamento esteja esgotado (daí o loop while dentro dos 'processadores' também).
J Jones
Qual é a linha "monitor_workers" no final do script de monitor?
J Jones
@JJones - monitor_workersé como process_file- é uma função que faz o que você quiser. Sobre o monitor - você estava certo; ele deve salvar os pids de seus trabalhadores (para que ele possa enviar um sinal de interrupção) e o contador precisa ser incrementado quando ele inicia um trabalhador. Eu editei a resposta para incluir isso.
Shawn J. Goff
Eu realmente aprecio o seu trabalho, mas acho que você deve usar o GNU parallel. Eu acho que é sua ideia, totalmente implementada.
Motobói
5

Outro exemplo:

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

Achei os outros exemplos desnecessariamente complexos, quando, na maioria dos casos, é o que você está procurando acima.

deceleratedcaviar
fonte
4

Uma ferramenta comumente disponível que pode fazer paralelização é o make. O GNU make e alguns outros têm uma -jopção para executar compilações paralelas.

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

Execute makeassim (presumo que os nomes dos seus arquivos não contenham caracteres especiais, makenão é bom com eles):

make -j 4 $(for x in *.input; do echo ${x%.*}.output; done)
Gilles 'SO- parar de ser mau'
fonte
IMHO este é o mais solução inteligente :)
h4unt3r
3

Isso é para executar o mesmo comando em um grande conjunto de arquivos no diretório atual:

#!/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

Isso executa o arquivo customScriptem cada txtarquivo, colocando a saída em outtxtarquivos. Mude conforme necessário. A chave para fazer isso funcionar é o processamento do sinal, usando SIGUSR1, para que o processo filho possa informar ao processo pai que está pronto. O uso do SIGCHLD não funcionará, pois a maioria das instruções no script gerará sinais do SIGCHLD para o script do shell. Eu tentei isso substituindo seu comando por sleep 1, o programa usava 0,28s de CPU do usuário e 0,14s de CPU do sistema; isso foi apenas em cerca de 400 arquivos.

Arcege
fonte
Como a 'espera' é esperta o suficiente para pegar o mesmo arquivo que está sendo iterado e reinserir a declaração "if" do irmão?
J Jones
Não é o waitque é 'inteligente' o suficiente; mas ele retornará após receber o SIGUSR1sinal. O filho / trabalhador envia um SIGUSR1para o pai, que é capturado ( trap) e diminui $worker( trapcláusula) e retorna anormalmente de wait, permitindo que a if [ $worker -lt $num_workers ]cláusula seja executada.
Arcege
0

Ou simplesmente use xargs -P, não é necessário instalar software adicional:

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

Um pouco de explicação para as opções:

  • -I'XXX' define a sequência que será substituída no modelo de comando pelo nome do arquivo
  • -P4 irá executar 4 processos em paralelo
  • -n1 colocará apenas um arquivo por execução, mesmo que dois XXX sejam encontrados
  • -print0e -0trabalhar juntos, permitindo que você tenha caracteres especiais (como espaço em branco) nos nomes dos arquivos
Piotr Czapla
fonte