Diffondere lo stdin su processi paralleli


13

Ho un compito che elabora un elenco di file su stdin. Il tempo di avvio del programma è notevole e il tempo impiegato da ciascun file varia notevolmente. Voglio generare un numero considerevole di questi processi, quindi inviare il lavoro a quelli che non sono occupati. Esistono diversi strumenti a riga di comando che fanno quasi quello che voglio, l'ho ridotto a due opzioni quasi funzionanti:

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

Il problema è che splitfa un round-robin puro, quindi uno dei processi rimane indietro e rimane indietro, ritardando il completamento dell'intera operazione; mentre parallelvuole generare un processo per N righe o byte di input e finisco per dedicare troppo tempo al sovraccarico di avvio.

Esiste qualcosa del genere che riutilizzerà i processi e le linee di alimentazione a qualunque processo abbia stdin sbloccati?


Da dove viene quel splitcomando? Il nome è in conflitto con l' utilità di elaborazione del testo standard .
Gilles 'SO- smetti di essere malvagio' il

@Gilles, è quello GNU: "split (GNU coreutils) 8.13" . Usarlo come una strana alternativa a xargs probabilmente non è l'uso previsto ma è il più vicino a quello che voglio che abbia trovato.
BCoates

2
Ci ho pensato e un problema fondamentale è sapere che un'istanza di myjobè pronta a ricevere più input. Non c'è modo di sapere che un programma è pronto per elaborare più input, tutto quello che puoi sapere è che qualche buffer da qualche parte (un buffer pipe, un buffer stdio) è pronto a ricevere più input. Puoi organizzare che il tuo programma invii qualche tipo di richiesta (es. Visualizzare un prompt) quando è pronto?
Gilles 'SO- smetti di essere malvagio' il

Supponendo che il programma non stia utilizzando il bufering su stdin, un filesystem FUSE che reagisce alle readchiamate farebbe il trucco. È uno sforzo di programmazione abbastanza ampio.
Gilles 'SO- smetti di essere malvagio' il

perché stai usando -l 1in parallelargs? IIRC, che dice al parallelo di elaborare una riga di input per lavoro (ovvero un nome file per fork di myjob, quindi un sacco di costi di avvio).
Cas

Risposte:


1

Ciò non sembra possibile in un caso così generale. Implica che hai un buffer per ogni processo e puoi guardare i buffer dall'esterno per decidere dove inserire la voce successiva (programmazione) ... Naturalmente potresti scrivere qualcosa (o usare un sistema batch come slurm)

Ma a seconda del processo, potresti essere in grado di pre-elaborare l'input. Ad esempio, se si desidera scaricare file, aggiornare voci da un DB o simili, ma il 50% di questi finirà per essere saltato (e quindi si ha una grande differenza di elaborazione a seconda dell'input), quindi impostare un pre-processore che verifica quali voci impiegheranno a lungo (il file esiste, i dati sono stati modificati, ecc.), quindi qualsiasi cosa provenga dall'altra parte è garantita per un tempo abbastanza uguale. Anche se l'euristica non è perfetta, potresti ottenere un notevole miglioramento. È possibile scaricare gli altri in un file ed elaborare successivamente nello stesso modo.

Ma questo dipende dal tuo caso d'uso.


1

No, non esiste una soluzione generica. Il tuo dispatcher deve sapere quando ogni programma è pronto a leggere un'altra riga e non sono a conoscenza di standard che lo consentano. Tutto quello che puoi fare è mettere una riga su STDOUT e attendere che qualcosa lo consumi; non c'è davvero un buon modo per il produttore su una pipeline di dire se il prossimo consumatore è pronto o no.


0

Io non la penso così. Nella mia rivista preferita c'era un articolo sulla programmazione bash che faceva quello che volevi. Sono disposto a credere che se ci fossero stati strumenti per farlo, li avrebbero menzionati. Quindi vuoi qualcosa sulla falsariga di:

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

Ovviamente puoi modificare l'invocazione allo script funzionante a tuo piacimento. La rivista che ho citato inizialmente fa cose come l'installazione di tubi e l'avvio di thread di lavoro. Check-outmkfifo a questo, ma quel percorso è molto più complicato in quanto i processi di lavoro devono segnalare al processo principale che sono pronti a ricevere più dati. Quindi è necessario un quindicesimo per ogni processo di lavoro per inviare i dati e un quindicesimo per il processo principale per ricevere roba dai lavoratori.

DISCLAIMER Ho scritto quella sceneggiatura dalla cima della mia testa. Potrebbe avere alcuni problemi di sintassi.


1
Questo non sembra soddisfare i requisiti: stai avviando un'istanza diversa del programma per ciascun elemento.
Gilles 'SO- smetti di essere malvagio'

Di solito è preferibile usare find . -type f | while read ipiuttosto che for i in $(find . -type f).

0

Per GNU Parallel puoi impostare la dimensione del blocco usando --block. Tuttavia, richiede una memoria sufficiente per mantenere 1 blocco in memoria per ciascuno dei processi in esecuzione.

Capisco che questo non è esattamente ciò che stai cercando, ma potrebbe essere un rimedio accettabile per ora.

Se le tue attività in media richiedono lo stesso tempo, potresti essere in grado di utilizzare mbuffer:

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

0

Prova questo:

mkfifo per ogni processo.

Quindi tail -f | myjobappendi ogni quindici.

Ad esempio, impostare i lavoratori (processi myjob)

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

A seconda dell'applicazione (myjob), è possibile utilizzare i lavori -s per trovare lavori interrotti. Altrimenti, elenca i processi ordinati per CPU e seleziona quello che consuma meno risorse. Di avere il rapporto di lavoro stesso, ad esempio impostando un flag nel file system quando vuole più lavoro.

Supponendo che il lavoro si interrompa durante l'attesa di input, utilizzare

jobs -sl per scoprire il pid di un lavoro interrotto e assegnarlo, ad esempio

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

Ho provato questo con

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 ammettere che è stato inventato, quindi ymmv.


0

Ciò che è veramente necessario per risolvere questo è un meccanismo di coda di qualche tipo.

È possibile che i lavori leggano il loro input da una coda, come una coda di messaggi SYSV, e quindi i programmi eseguiti in parallelo semplicemente spingano i valori sulla coda?

Un'altra possibilità è quella di utilizzare una directory per la coda, in questo modo:

  1. l'output find crea un collegamento simbolico a ciascun file da elaborare in una directory, pending
  2. ogni processo di lavoro esegue uno mvdei primi file che vede nella directory in una directory di pari livello pending, denominata inprogress.
  3. se il lavoro sposta correttamente il file, esegue l'elaborazione; in caso contrario, torna indietro per trovare e spostare un altro nome file dapending

0

spiegando la risposta di @ ash, è possibile utilizzare una coda di messaggi SYSV per distribuire il lavoro. Se non vuoi scrivere il tuo programma in C, c'è un'utilità chiamata ipcmdche può aiutarti. Ecco cosa ho messo insieme per passare l'output find $DIRECTORY -type fal $PARALLELnumero di processi:

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

Ecco una corsa di prova:

$ 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 meno che non sia possibile stimare per quanto tempo verrà elaborato un determinato file di input e i processi di lavoro non hanno un modo per riportare allo scheduler (come fanno in normali scenari di calcolo parallelo - spesso tramite MPI ), si è generalmente sfortunati - pagare la penalità di alcuni lavoratori che elaborano input più a lungo di altri (a causa della disparità di input) oppure pagare la sanzione della generazione di un nuovo processo per ogni file di input.


0

GNU Parallel è cambiato negli ultimi 7 anni. Quindi oggi può farlo:

Questo esempio mostra che vengono dati più blocchi al processo 11 e 10 rispetto al processo 4 e 5 perché 4 e 5 leggono più lentamente:

seq 1000000 |
  parallel -j8 --tag --roundrobin --pipe --block 1k 'pv -qL {}0000 | wc' ::: 11 4 5 6 9 8 7 10
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.