Quatro tarefas em paralelo ... como faço isso?

23

Eu tenho um monte de imagens PNG em um diretório. Eu tenho um aplicativo chamado pngout que eu corro para compactar essas imagens. Este aplicativo é chamado por um script que eu fiz. O problema é que esse script executa um de cada vez, algo como isto:

FILES=(./*.png)
for f in  "${FILES[@]}"
do
        echo "Processing $f file..."
        # take action on each file. $f store current file name
        ./pngout -s0 $f R${f/\.\//}
done

Processar apenas um arquivo por vez, leva muito tempo. Depois de executar este aplicativo, vejo que a CPU é de apenas 10%. Então, descobri que posso dividir esses arquivos em 4 lotes, colocar cada lote em um diretório e disparar 4, de quatro janelas de terminal, quatro processos, para que eu tenha quatro instâncias do meu script, ao mesmo tempo, processando essas imagens e as o trabalho leva 1/4 do tempo.

O segundo problema é que perdi tempo dividindo as imagens e lotes e copiando o script para quatro diretórios, abrindo 4 janelas de terminal, bla bla ...

Como fazer isso com um script, sem ter que dividir nada?

Quero dizer duas coisas: primeiro, como faço a partir de um script bash, aciono um processo em segundo plano? (basta adicionar & ao final?) Segundo: como eu paro de enviar tarefas para segundo plano depois de enviar a quarta tarefa e coloco o script em espera até que as tarefas terminem? Quero dizer, apenas enviando uma nova tarefa para o plano de fundo quando uma tarefa termina, mantendo sempre quatro tarefas em paralelo? se eu não fizer isso, o loop disparará zilhões de tarefas em segundo plano e a CPU ficará obstruída.

Cao espacial
fonte
Veja também Paralelizando um loop for
Gilles 'SO- stop be evil'

Respostas:

33

Se você possui uma cópia xargscompatível com a execução paralela -P, basta fazer

printf '%s\0' *.png | xargs -0 -I {} -P 4 ./pngout -s0 {} R{}

Para outras idéias, o wiki do Wooledge Bash possui uma seção no artigo Gerenciamento de processos que descreve exatamente o que você deseja.

jw013
fonte
2
Existem também "gnu paralelo" e "xjobs" projetados para este caso. É principalmente uma questão de gosto que você prefere.
Wnoise
Você poderia explicar o comando proposto? Obrigado!
Eugene S
1
@EugeneS Você poderia ser um pouco mais específico sobre qual parte? O printf coleta todos os arquivos png e os passa por um canal para xargs, que coleta argumentos da entrada padrão e os combina em argumentos para o pngoutcomando que o OP queria executar. A opção principal é -P 4, que diz ao xargs para usar até 4 comandos simultâneos.
Jw013
2
Desculpe por não ser preciso. Eu estava especificamente interessado por que você usou a printffunção aqui em vez de apenas regular ls .. | grep .. *.png? Também estava interessado nos xargsparâmetros que você usou ( -0e -I{}). Obrigado!
Eugene S
3
@EugeneS É para máxima correção e robustez. Os nomes de arquivos não são linhas e lsnão podem ser usados ​​para analisar nomes de arquivos de maneira portável e segura . Os únicos caracteres seguros a serem usados ​​para delimitar nomes de arquivos são \0e /, já que todos os outros caracteres, inclusive \n, podem fazer parte do próprio nome do arquivo. Os printfusos \0para os nomes de arquivos delimitar, e os -0informa xargsdeste. O -I{}diz xargspara substituir {}com o argumento.
Jw013
8

Além das soluções já propostas, você pode criar um makefile que descreva como criar um arquivo compactado a partir de descompactado e usá-lo make -j 4para executar 4 trabalhos em paralelo. O problema é que você precisará nomear os arquivos compactados e descompactados de maneira diferente ou armazená-los em diretórios diferentes; caso contrário, será impossível escrever uma regra de fabricação razoável.

9000
fonte
5

Para responder suas duas perguntas:

  • Sim, adicionar & no final da linha instruirá o shell a iniciar um processo em segundo plano.
  • usando o waitcomando, você pode pedir ao shell para aguardar a conclusão de todos os processos em segundo plano antes de prosseguir.

Aqui está o script modificado para que jseja usado para acompanhar o número de processos em segundo plano. Quando NB_CONCURRENT_PROCESSESatingido, o script será redefinido jpara 0 e aguardará a conclusão de todos os processos em segundo plano antes de retomar sua execução.

files=(./*.png)
nb_concurrent_processes=4
j=0
for f in "${files[@]}"
do
        echo "Processing $f file..."
        # take action on each file. $f store current file name
        ./pngout -s0 "$f" R"${f/\.\//}" &
        ((++j == nb_concurrent_processes)) && { j=0; wait; }
done
Frederik Deweerdt
fonte
1
Isso aguardará o último dos quatro processos simultâneos e iniciará um conjunto de outros quatro. Talvez deva-se construir uma matriz de quatro PIDs e esperar por esses PIDs específicos?
Nils
Apenas para explicar minhas correções no código: (1) Por uma questão de estilo, evite todos os nomes de variáveis ​​em maiúsculas, pois eles podem entrar em conflito com as variáveis ​​internas do shell. (2) Citação adicionada para $fetc. (3) Use [para scripts compatíveis com POSIX, mas para o bash puro [[é sempre preferido. Nesse caso, ((é mais apropriado para a aritmética.
Jw013 31/03/12