Stdin auf parallele Prozesse verteilen


13

Ich habe eine Aufgabe, die eine Liste von Dateien auf stdin verarbeitet. Die Startzeit des Programms ist beträchtlich und die Zeit, die jede Datei benötigt, ist sehr unterschiedlich. Ich möchte eine beträchtliche Anzahl dieser Prozesse erzeugen und dann die Arbeit an diejenigen senden, die nicht beschäftigt sind. Es gibt verschiedene Kommandozeilen-Tools, die beinahe das tun, was ich will. Ich habe sie auf zwei beinahe funktionierende Optionen eingegrenzt:

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

Das Problem ist, dass splitein reines Round-Robin-Verfahren durchgeführt wird, sodass einer der Prozesse zurückbleibt und den Abschluss der gesamten Operation verzögert. while parallelmöchte einen Prozess pro N Zeilen oder Bytes an Eingaben erzeugen, und ich verbringe viel zu viel Zeit mit dem Start-Overhead.

Gibt es so etwas, das die Prozesse und Zuleitungen zu den Prozessen wiederverwendet, bei denen die Blockierung aufgehoben wurde?


Woher kommt dieser splitBefehl? Der Name steht in Konflikt mit dem Standarddienstprogramm für die Textverarbeitung.
Gilles 'SO- hör auf böse zu sein'

@ Gilles, es ist die GNU: "split (GNU coreutils) 8.13" . Es als seltsame Alternative zu xargs zu benutzen, ist wahrscheinlich nicht die beabsichtigte Verwendung, aber es kommt dem, was ich gefunden habe, am nächsten.
BCoates

2
Ich habe darüber nachgedacht, und ein grundlegendes Problem besteht darin, zu wissen, dass eine Instanz von myjobbereit ist, mehr Input zu erhalten. Es gibt keine Möglichkeit zu wissen, dass ein Programm bereit ist, mehr Eingaben zu verarbeiten. Sie können nur wissen, dass ein Puffer (ein Pipe-Puffer, ein Stdio-Puffer) bereit ist, mehr Eingaben zu empfangen. Können Sie veranlassen, dass Ihr Programm eine Anfrage sendet (z. B. eine Eingabeaufforderung anzeigt), wenn es fertig ist?
Gilles 'SO - hör auf böse zu sein'

Angenommen, das Programm verwendet keine Pufferung für stdin, würde ein FUSE-Dateisystem, das auf readAufrufe reagiert, den Trick ausführen. Das ist ein ziemlich großer Programmieraufwand.
Gilles 'SO - hör auf, böse zu sein'

Warum benutzt du -l 1in den parallelArgs? IIRC, das parallel anweist, eine Eingabezeile pro Job zu verarbeiten (dh einen Dateinamen pro Fork von myjob, also viel Start-Overhead).
cas

Antworten:


1

Das scheint in einem so allgemeinen Fall nicht möglich zu sein. Es bedeutet, dass Sie für jeden Prozess einen Puffer haben und die Puffer von außen beobachten können, um zu entscheiden, wo der nächste Eintrag abgelegt werden soll (Planung). Natürlich können Sie etwas schreiben (oder ein Batch-System wie slurm verwenden)

Je nachdem, um welchen Prozess es sich handelt, können Sie die Eingabe möglicherweise vorverarbeiten. Wenn Sie zum Beispiel Dateien herunterladen, Einträge aus einer Datenbank aktualisieren oder Ähnliches, aber 50% davon übersprungen werden (und Sie haben daher einen großen Verarbeitungsunterschied, der von der Eingabe abhängt), richten Sie einfach einen Vorprozessor ein Damit wird überprüft, welche Einträge lange dauern werden (Datei vorhanden, Daten wurden geändert usw.), sodass alles, was von der anderen Seite kommt, garantiert eine relativ lange Zeit in Anspruch nimmt. Auch wenn die Heuristik nicht perfekt ist, kann dies zu einer erheblichen Verbesserung führen. Sie können die anderen in eine Datei sichern und anschließend auf die gleiche Weise verarbeiten.

Das hängt jedoch von Ihrem Anwendungsfall ab.


1

Nein, es gibt keine generische Lösung. Ihr Dispatcher muss wissen, wann jedes Programm bereit ist, eine andere Zeile zu lesen, und es gibt keinen mir bekannten Standard, der dies zulässt. Alles, was Sie tun können, ist, eine Zeile auf STDOUT zu setzen und darauf zu warten, dass etwas davon verbraucht wird. Es gibt keine gute Möglichkeit für den Produzenten in einer Pipeline zu erkennen, ob der nächste Verbraucher bereit ist oder nicht.


0

Ich glaube nicht. In meinem Lieblingsmagazin war einmal ein Artikel über Bash-Programmierung, der tat, was Sie wollten. Ich bin gewillt zu glauben, dass sie diese Werkzeuge erwähnt hätten, wenn es sie gegeben hätte. Sie möchten also etwas im Sinne von:

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

Natürlich können Sie den Aufruf des aktuellen Skripts nach Ihren Wünschen ändern. Die Zeitschrift, die ich erwähne, macht anfangs Dinge wie das Einrichten von Pipes und das eigentliche Starten von Worker-Threads. Schauen Sie sich mkfifodas an, aber diese Route ist weitaus komplizierter, da die Worker-Prozesse dem Master-Prozess signalisieren müssen, dass sie bereit sind, weitere Daten zu empfangen. Sie benötigen also ein FIFO für jeden Worker-Prozess, um die Daten zu senden, und ein FIFO für den Master-Prozess, um die Daten der Worker zu empfangen.

HAFTUNGSAUSSCHLUSS Ich habe das Drehbuch von oben geschrieben. Möglicherweise gibt es einige Syntaxprobleme.


1
Dies scheint die Anforderungen nicht zu erfüllen: Sie starten für jedes Element eine andere Instanz des Programms.
Gilles 'SO- hör auf böse zu sein'

Es ist normalerweise vorzuziehen, find . -type f | while read ianstatt zu verwenden for i in $(find . -type f).

0

Für GNU Parallel können Sie die Blockgröße mit --block einstellen. Es ist jedoch erforderlich, dass Sie über genügend Speicher verfügen, um für jeden der ausgeführten Prozesse einen Block im Speicher zu behalten.

Ich verstehe, dass dies nicht genau das ist, wonach Sie suchen, aber es könnte vorerst eine akzeptable Lösung sein.

Wenn Ihre Aufgaben im Durchschnitt dieselbe Zeit in Anspruch nehmen, können Sie möglicherweise mbuffer verwenden:

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

0

Versuche dies:

mkfifo für jeden Prozess.

Dann hängen Sie tail -f | myjoban jedem FIFO.

Zum Beispiel das Einrichten der Arbeiter (Myjob-Prozesse)

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

Abhängig von Ihrer Anwendung (myjob) können Sie Jobs verwenden, um angehaltene Jobs zu finden. Andernfalls listen Sie die Prozesse nach CPU sortiert auf und wählen Sie denjenigen aus, der am wenigsten Ressourcen verbraucht. Von sich aus muss der Job gemeldet werden, zB indem im Dateisystem ein Flag gesetzt wird, wenn mehr Arbeit gewünscht wird.

Unter der Annahme, dass der Job beim Warten auf eine Eingabe stoppt, verwenden Sie

jobs -sl Zum Beispiel, um die PID eines gestoppten Jobs herauszufinden und ihm eine Arbeit zuzuweisen

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

Ich habe das mit getestet

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

Dies muss ich zugeben, wurde nur so ymmv zusammengebraut.


0

Was wirklich benötigt wird, um dies zu lösen, ist ein Warteschlangenmechanismus irgendeiner Art.

Ist es möglich, dass die Jobs ihre Eingaben aus einer Warteschlange lesen, z. B. einer SYSV-Nachrichtenwarteschlange, und die Programme dann parallel ausgeführt werden, indem die Werte einfach in die Warteschlange verschoben werden?

Eine andere Möglichkeit ist die Verwendung eines Verzeichnisses für die Warteschlange:

  1. Die Ausgabe von find erstellt einen Symlink zu jeder Datei, die in einem Verzeichnis verarbeitet werden soll. pending
  2. Jeder Jobprozess führt eine mvder ersten Dateien aus, die er im Verzeichnis sieht, und zwar in ein gleichrangiges Verzeichnis mit dem pendingNamen inprogress.
  3. Wenn der Job die Datei erfolgreich verschiebt, führt er die Verarbeitung durch. Andernfalls wird ein anderer Dateiname gesucht und verschobenpending

0

Wenn Sie die Antwort von @ ash erläutern, können Sie eine SYSV-Nachrichtenwarteschlange verwenden, um die Arbeit zu verteilen. Wenn Sie kein eigenes Programm in C schreiben möchten, gibt es ein Hilfsprogramm, das Ihnen ipcmdhelfen kann. Folgendes habe ich zusammengestellt, um die Ausgabe find $DIRECTORY -type fan die $PARALLELAnzahl der Prozesse zu übergeben:

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

Hier ist ein Testlauf:

$ 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

Wenn Sie nicht abschätzen können, wie lange eine bestimmte Eingabedatei verarbeitet wird und die Worker-Prozesse keine Möglichkeit haben, dem Scheduler Bericht zu erstatten (wie in normalen Parallel-Computing-Szenarien - häufig über MPI ), haben Sie im Allgemeinen Pech - entweder die Strafe dafür zahlen, dass einige Mitarbeiter Eingaben länger verarbeiten als andere (wegen der Ungleichheit der Eingaben), oder die Strafe dafür zahlen, dass für jede Eingabedatei ein neuer Prozess erstellt wird.


0

GNU Parallel hat sich in den letzten 7 Jahren verändert. So kann es heute sein:

Dieses Beispiel zeigt, dass Prozess 11 und 10 mehr Blöcke erhalten als Prozess 4 und 5, da 4 und 5 langsamer lesen:

seq 1000000 |
  parallel -j8 --tag --roundrobin --pipe --block 1k 'pv -qL {}0000 | wc' ::: 11 4 5 6 9 8 7 10
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.