Espalhando processos stdin para paralelos


13

Eu tenho uma tarefa que processa uma lista de arquivos no stdin. O tempo de inicialização do programa é substancial e a quantidade de tempo que cada arquivo leva varia muito. Quero gerar um número substancial desses processos e depois enviar o trabalho para os que não estiverem ocupados. Existem várias ferramentas diferentes de linha de comando que quase fazem o que eu quero, reduzi-o a duas opções quase funcionais:

find . -type f | split -n r/24 -u --filter="myjob"
find . -type f | parallel --pipe -u -l 1 myjob

O problema é que splitele faz um round-robin puro, para que um dos processos fique para trás e fique para trás, atrasando a conclusão de toda a operação; enquanto parallelquer gerar um processo por N linhas ou bytes de entrada e acabo gastando muito tempo na sobrecarga da inicialização.

Existe algo assim que reutilizará os processos e linhas de alimentação para quaisquer processos que tenham stdins desbloqueados?


De onde é esse splitcomando? O nome entra em conflito com o utilitário de processamento de texto padrão .
Gilles 'SO- stop be evil'

@Gilles, é o GNU: "split (GNU coreutils) 8.13" . Usá-lo como uma alternativa estranha ao xargs provavelmente não é o uso pretendido, mas é o mais próximo do que eu quero que encontrei.
BCoates # 9/12

2
Eu estive pensando sobre isso, e um problema fundamental é saber que uma instância de myjobestá pronta para receber mais informações. Não há como saber que um programa está pronto para processar mais entradas, tudo que você pode saber é que algum buffer em algum lugar (um buffer de pipe, um buffer de stdio) está pronto para receber mais entradas. Você pode providenciar para o seu programa enviar algum tipo de solicitação (por exemplo, exibir um prompt) quando estiver pronto?
Gilles 'SO- stop being evil'

Supondo que o programa não esteja usando o bufering no stdin, um sistema de arquivos do FUSE que reaja às readchamadas faria o truque. Esse é um empreendimento de programação bastante grande.
Gilles 'SO- stop being evil'

por que você está usando -l 1nos parallelargumentos? IIRC, que diz paralelamente para processar uma linha de entrada por trabalho (ou seja, um nome de arquivo por bifurcação de myjob, com muita sobrecarga de inicialização).
cas

Respostas:


1

Isso não parece possível em um caso tão geral. Isso implica que você tem um buffer para cada processo e pode assistir os buffers de fora para decidir onde colocar a próxima entrada (programação) ... É claro que você pode escrever algo (ou usar um sistema em lote como slurm)

Mas, dependendo do processo, você poderá pré-processar a entrada. Por exemplo, se você deseja baixar arquivos, atualizar entradas de um banco de dados ou similar, mas 50% deles serão ignorados (e, portanto, você terá uma grande diferença de processamento dependendo da entrada), basta configurar um pré-processador que verifica quais entradas levarão muito tempo (arquivo existe, dados foram alterados etc.), para garantir que o que vier do outro lado leve um tempo bastante igual. Mesmo que a heurística não seja perfeita, você poderá obter uma melhoria considerável. Você pode despejar os outros em um arquivo e processar posteriormente da mesma maneira.

Mas isso depende do seu caso de uso.


1

Não, não há uma solução genérica. Seu despachante precisa saber quando cada programa está pronto para ler outra linha, e não há nenhum padrão que eu saiba que permita isso. Tudo o que você pode fazer é colocar uma linha em STDOUT e esperar que algo a consuma; não há realmente uma boa maneira de o produtor em um pipeline saber se o próximo consumidor está pronto ou não.


0

Acho que não. Na minha revista favorita, havia um artigo sobre programação de bash que fazia o que você queria. Estou disposto a acreditar que, se houvesse ferramentas para isso, eles as mencionariam. Então você quer algo parecido com:

set -m # enable job control
max_processes=8
concurrent_processes=0

child_has_ended() { concurrent_processes=$((concurrent_processes - 1)) }

trap child_has_ended SIGCHLD # that's magic calling our bash function when a child processes ends

for i in $(find . -type f)
do
  # don't do anything while there are max_processes running
  while [ ${concurrent_processes} -ge ${max_processes}]; do sleep 0.5; done 
  # increase the counter
  concurrent_processes=$((concurrent_processes + 1))
  # start a child process to actually deal with one file
  /path/to/script/to/handle/one/file $i &
done

Obviamente, você pode alterar a invocação para o script de trabalho real ao seu gosto. A revista que mencionei inicialmente faz coisas como configurar tubos e realmente iniciar threads de trabalho. Verifique mkfifoisso, mas essa rota é muito mais complicada, pois os processos de trabalho precisam sinalizar ao processo mestre que estão prontos para receber mais dados. Portanto, você precisa de um fifo para cada processo do trabalhador para enviar dados e um fifo para o processo mestre para receber informações dos trabalhadores.

AVISO LEGAL Eu escrevi esse roteiro do alto da minha cabeça. Pode haver alguns problemas de sintaxe.


1
Parece que isso não atende aos requisitos: você está iniciando uma instância diferente do programa para cada item.
Gilles 'SO- stop be evil'

Geralmente é preferível usar find . -type f | while read ie não for i in $(find . -type f).

0

Para o GNU Parallel, você pode definir o tamanho do bloco usando --block. No entanto, exige que você tenha memória suficiente para manter 1 bloco na memória para cada um dos processos em execução.

Entendo que não é exatamente isso que você está procurando, mas pode ser uma solução aceitável por enquanto.

Se, em média, suas tarefas durarem o mesmo tempo, você poderá usar o mbuffer:

find . -type f | split -n r/24 -u --filter="mbuffer -m 2G | myjob"

0

Tente o seguinte:

mkfifo para cada processo.

Depois, aguarde tail -f | myjobcada quino.

Por exemplo, configurando os trabalhadores (processos do meu emprego)

mkdir /tmp/jobs
for X in 1 2 3 4
do
   mkfifo pipe$X
   tail -f pipe$X | myjob &
   jobs -l| awk '/pipe'$X'/ {print $2, "'pipe$X'"}' >> pipe-job-mapping
done

Dependendo do seu aplicativo (myjob), você poderá usar os trabalhos -s para encontrar trabalhos interrompidos. Caso contrário, liste os processos classificados por CPU e selecione o que consome menos recursos. De ter o relatório de trabalho em si, por exemplo, definindo um sinalizador no sistema de arquivos quando ele quer mais trabalho.

Supondo que o trabalho seja interrompido ao aguardar entrada, use

jobs -sl descobrir pid de um trabalho interrompido e atribuí-lo a trabalho, por exemplo

grep "^$STOPPED_PID" pipe-to-job-mapping | while read PID PIPE
do
   cat workset > $PIPE
done

Eu testei isso com

garfield:~$ cd /tmp
garfield:/tmp$ mkfifo f1
garfield:/tmp$ mkfifo f2
garfield:/tmp$ tail -f f1 | sed 's/^/1 /' &
[1] 21056
garfield:/tmp$ tail -f f2 | sed 's/^/2 /' &
[2] 21058
garfield:/tmp$ echo hello > f1
1 hello
garfield:/tmp$ echo what > f2
2 what
garfield:/tmp$ echo yes > f1
1 yes

Devo admitir que isso foi inventado de forma tão hummm.


0

O que é realmente necessário para resolver isso é um mecanismo de fila de algum tipo.

É possível que os trabalhos leiam suas entradas de uma Fila, como uma fila de mensagens SYSV, e os programas sejam executados em paralelo, basta enviar os valores para a fila?

Outra possibilidade é usar diretórios para a fila, assim:

  1. a saída find cria um link simbólico para cada arquivo para processar em um diretório, pending
  2. cada processo de trabalho executa um mvdos primeiros arquivos que vê no diretório para um diretório irmão de pending, nomeado inprogress.
  3. se o trabalho mover com êxito o arquivo, ele executará o processamento; caso contrário, ele volta a encontrar e mover outro nome de arquivo depending

0

expondo a resposta do @ ash, você pode usar uma fila de mensagens SYSV para distribuir o trabalho. Se você não quiser escrever seu próprio programa em C, existe um utilitário chamado ipcmdque pode ajudar. Aqui está o que eu reuni para passar a saída find $DIRECTORY -type fpara o $PARALLELnúmero de processos:

set -o errexit
set -o nounset

export IPCMD_MSQID=$(ipcmd msgget)

DIRECTORY=$1
PARALLEL=$2

# clean up message queue on exit
trap 'ipcrm -q $IPCMD_MSQID' EXIT

for i in $(seq $PARALLEL); do
   {
      while true
      do
          message=$(ipcmd msgrcv) || exit
          [ -f $message ] || break
          sleep $((RANDOM/3000))
      done
   } &
done

find "$DIRECTORY" -type f | xargs ipcmd msgsnd

for i in $(seq $PARALLEL); do
   ipcmd msgsnd "/dev/null/bar"
done
wait

Aqui está um teste:

$ for i in $(seq 20 10 100) ; do time parallel.sh /usr/lib/ $i ; done
parallel.sh /usr/lib/ $i  0.30s user 0.67s system 0% cpu 1:57.23 total
parallel.sh /usr/lib/ $i  0.28s user 0.69s system 1% cpu 1:09.58 total
parallel.sh /usr/lib/ $i  0.19s user 0.80s system 1% cpu 1:05.29 total
parallel.sh /usr/lib/ $i  0.29s user 0.73s system 2% cpu 44.417 total
parallel.sh /usr/lib/ $i  0.25s user 0.80s system 2% cpu 37.353 total
parallel.sh /usr/lib/ $i  0.21s user 0.85s system 3% cpu 32.354 total
parallel.sh /usr/lib/ $i  0.30s user 0.82s system 3% cpu 28.542 total
parallel.sh /usr/lib/ $i  0.27s user 0.88s system 3% cpu 30.219 total
parallel.sh /usr/lib/ $i  0.34s user 0.84s system 4% cpu 26.535 total

0

A menos que você possa estimar quanto tempo um arquivo de entrada específico será processado e os processos de trabalho não terão como relatar ao agendador (como fazem em cenários normais de computação paralela - geralmente por meio do MPI ), você geralmente não tem sorte. - pagar a penalidade de alguns trabalhadores processarem insumos por mais tempo do que outros (devido à desigualdade de insumos) ou pagar a penalidade de gerar um único processo novo para cada arquivo de insumos.


0

O GNU Parallel mudou nos últimos 7 anos. Então hoje ele pode fazer isso:

Este exemplo mostra que mais blocos são dados ao processo 11 e 10 do que ao processo 4 e 5 porque 4 e 5 são lidos mais lentamente:

seq 1000000 |
  parallel -j8 --tag --roundrobin --pipe --block 1k 'pv -qL {}0000 | wc' ::: 11 4 5 6 9 8 7 10
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.