Paralelizar um loop Bash FOR

109

Eu tenho tentado paralelizar o script a seguir, especificamente cada uma das três instâncias de loop FOR, usando o GNU Parallel, mas não consegui. Os 4 comandos contidos no loop FOR são executados em série, cada loop levando cerca 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
Ravnoor S Gill
fonte

Respostas:

94

Por que você não os bifurca (também conhecido como background)?

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

Caso isso não esteja claro, a parte significativa está aqui:

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

Fazendo com que a função seja executada em um shell bifurcado em segundo plano. Isso é paralelo.

Cachinhos Dourados
fonte
6
Isso funcionou como um encanto. Obrigado. Uma implementação tão simples (me faz sentir tão estúpido agora!).
Ravnoor S Gill
8
No caso de eu ter 8 arquivos para executar em paralelo, mas apenas 4 núcleos, isso poderia ser integrado em uma configuração ou exigiria um Job Scheduler?
Ravnoor S Gill,
6
Realmente não importa nesse contexto; é normal que o sistema tenha processos mais ativos que núcleos. Se você tiver muitas tarefas curtas , o ideal seria alimentar uma fila atendida por um número ou threads de trabalho <o número de núcleos. Não sei com que frequência isso é realmente feito com scripts de shell (nesse caso, eles não seriam threads, seriam processos independentes), mas com relativamente poucas tarefas longas , seria inútil. O agendador do sistema operacional cuidará deles.
Goldilocks
17
Você também pode querer adicionar um waitcomando no final para que o script mestre não saia até que todos os trabalhos em segundo plano o façam.
Psusi
1
Eu também consideraria útil limitar o número de processos simultâneos: meus processos usam cada um 100% do tempo de um núcleo por cerca de 25 minutos. Este é um servidor compartilhado com 16 núcleos, onde muitas pessoas estão executando trabalhos. Eu preciso executar 23 cópias do script. Se eu executá-los todos simultaneamente, eu permuto o servidor e o inutilizo para todos os outros por uma ou duas horas (a carga sobe para 30, tudo fica mais lento). Eu acho que isso poderia ser feito com nice, mas então eu não sei se alguma vez terminar ..
naught101
150

Tarefa de amostra

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

Execuções sequenciais

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

Execuções paralelas

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

Execuções paralelas em lotes do processo N

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

Também é possível usar FIFOs como semáforos e usá-los para garantir que novos processos sejam gerados o mais rápido possível e que não mais que N processos sejam executados ao mesmo tempo. Mas isso requer mais código.

N processos com um semáforo baseado em 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 
PSkocik
fonte
4
A linha com waitele basicamente permite que todos os processos sejam executados, até atingir o nthprocesso, e aguarda que todos os outros terminem de executar, certo?
naught101
Se ifor zero, ligue para aguardar. Incremento iapós o teste zero.
PSKocik #
2
@ naught101 Sim. waitw / no arg aguarda todas as crianças. Isso torna um pouco inútil. A abordagem baseada em tubo de semáforo dá-lhe concorrência mais fluente (Eu tenho usado isso em um sistema de compilação com base shell personalizado junto com -nt/ -otcheques com sucesso por um tempo agora)
PSkocik
1
@ BeowulfNode42 Você não precisa sair. O status de retorno da tarefa não prejudicará a consistência do semáforo, desde que o status (ou algo com esse comprimento de byte) seja gravado de volta ao fifo depois que o processo da tarefa sair / travar.
PSKocik
1
Para sua informação, o mkfifo pipe-$$comando precisa de acesso de gravação apropriado ao diretório atual. Portanto, prefiro especificar o caminho completo, como /tmp/pipe-$$provavelmente o acesso de gravação disponível para o usuário atual, em vez de depender do diretório atual. Sim, substitua todas as 3 ocorrências de pipe-$$.
BeowulfNode42 29/07
65
for stuff in things
do
( something
  with
  stuff ) &
done
wait # for all the something with stuff

Se ele realmente funciona depende de seus comandos; Eu não estou familiarizado com eles. O rm *.matparece um pouco propenso a conflitos se ele é executado em paralelo ...

frostschutz
fonte
2
Isso funciona perfeitamente também. Você está certo, eu precisaria mudar rm *.matpara algo como rm $run".mat"fazê-lo funcionar sem que um processo interfira no outro. Obrigado .
Ravnoor S Gill
@RavnoorSGill Bem-vindo ao Stack Exchange! Se esta resposta resolveu seu problema, marque-o como aceito , marcando a caixa de seleção ao lado.
Gilles
7
+1 para wait, que eu esqueci.
Goldilocks
5
Se houver toneladas de 'coisas', isso não iniciará toneladas de processos? Seria melhor iniciar apenas um número sadio de processos simultaneamente, certo?
David Doria
1
Dica muito útil! Como configurar o número de threads neste caso?
Dadong Zhang
30
for stuff in things
do
sem -j+0 ( something
  with
  stuff )
done
sem --wait

Isso usará semáforos, paralelizando tantas iterações quanto o número de núcleos disponíveis (-j +0 significa que você paralelizará N + 0 trabalhos , onde N é o número de núcleos disponíveis ).

o sem --wait diz para esperar até que todas as iterações no loop for tenham terminado a execução antes de executar as sucessivas linhas de código.

Nota: você precisará "paralelamente" do projeto paralelo GNU (sudo apt-get install parallel).

lev
fonte
1
é possível passar dos 60? o meu gera um erro dizendo que não há descritores de arquivo suficientes.
chovy
Se isso estiver gerando um erro de sintaxe por causa dos aparelhos para qualquer pessoa também, dê uma olhada na resposta de moritzschaefer.
Nicolai
10

Uma maneira realmente fácil que eu costumo usar:

cat "args" | xargs -P $NUM_PARALLEL command

Isso executará o comando, passando em cada linha do arquivo "args", paralelamente, executando no máximo $ NUM_PARALLEL ao mesmo tempo.

Você também pode procurar na opção -I xargs, se precisar substituir os argumentos de entrada em locais diferentes.

eyeApps LLC
fonte
6

Parece que os trabalhos fsl dependem um do outro, portanto, os 4 trabalhos não podem ser executados em paralelo. As execuções, no entanto, podem ser executadas em paralelo.

Faça uma função bash executando uma única execução e execute essa função em 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 saber mais, assista aos vídeos de introdução: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1 e passe uma hora percorrendo o tutorial http://www.gnu.org/software/parallel/parallel_tutorial.html Seu comando linha vai te amar por isso.

Ole Tange
fonte
Se você estiver usando um shell que não seja do bash, precisará também export SHELL=/bin/bashantes de executar em paralelo. Caso contrário, você receberá um erro como:Unknown command 'myfunc arg'
AndrewHarvey
1
@AndrewHarvey: não é para isso que serve?
naught101
5

Execução paralela no máximo processo N simultâneo

#!/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"
Tomasz Hławiczka
fonte
3

Eu realmente gosto da resposta do @lev, pois fornece controle sobre o número máximo de processos de uma maneira muito simples. No entanto, conforme descrito no manual , o sem não funciona com colchetes.

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

Faz o trabalho.

-j + N Adicione N ao número de núcleos da CPU. Execute vários trabalhos em paralelo. Para tarefas intensivas de computação, -j +0 é útil, pois executará tarefas de número de núcleos de CPU simultaneamente.

-j -N Subtrai N do número de núcleos da CPU. Execute vários trabalhos em paralelo. Se o número avaliado for menor que 1, 1 será usado. Veja também --use-cpus-invés de núcleos.

moritzschaefer
fonte
1

No meu caso, como não posso usar o semáforo (estou no git-bash no Windows), criei uma maneira genérica de dividir a tarefa entre os N trabalhadores, antes que eles comecem.

Funciona bem se as tarefas levarem aproximadamente a mesma quantidade de tempo. A desvantagem é que, se um dos trabalhadores demorar muito para fazer sua parte do trabalho, os outros que já terminaram não ajudarão.

Divisão do trabalho entre N trabalhadores (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
geekley
fonte
0

Eu tive problemas com @PSkocika solução. Meu sistema não possui o GNU Parallel disponível como pacote e semlançou uma exceção quando eu o criei e o executei manualmente. Tentei também o exemplo de semáforo FIFO, que também gerou alguns outros erros em relação à comunicação.

@eyeApps xargs sugerido, mas eu não sabia como fazê-lo funcionar com meu caso de uso complexo (exemplos seriam bem-vindos).

Aqui está minha solução para trabalhos paralelos que processam até Ntrabalhos por vez, conforme 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
}

Exemplo 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
Zhro
fonte