Discussion:
bash: Nebenwirkungen von jobs?
(zu alt für eine Antwort)
Christian Garbs
2019-11-13 22:08:29 UTC
Permalink
Mahlzeit!

Ich möchte mehrere Kommandos in begrenzter Menge parallel laufen
lassen, was mit folgendem Bash-Skript auch klappt: (*)

#v+

#!/bin/bash

MAX_JOBS=3

long_running_command()
{
local ID=$1
echo "$ID start"
sleep 0.$ID
echo " $ID stop"
}

wait_max()
{
local MAX=$1
while [ $(jobs | wc -l) -ge $MAX ]; do
jobs >/dev/null # ???
sleep 0.1
done
}

for I in $(seq 9); do
wait_max $MAX_JOBS
long_running_command $I &
done

wait_max 1
echo finished

#v-


Wenn ich allerdings die mit # ??? markierte Zeile auskommentiere, dann
läuft das Skript endlos weiter und beendet sich nicht.

Warum muss das jobs-Kommando zweimal aufgerufen werden, damit das
Skript funktioniert?

Eigentlich sollte das ja nur den Status aller laufenden Jobs ausgeben,
aber es scheint noch Nebenwirkungen zu geben. Warum und welche genau?


Gruß
Christian


(*) Eigentlich wollte ich GNU Parallel benutzen, aber in dem
Container, wo das Skript laufen soll, gibt es kein Perl.
Auf der Suche nach Alternativen habe ich
https://github.com/kou1okada/lesser-parallel gefunden, woraus ich
die paar relevanten Zeilen für das obige Skript extrahiert habe.
Auch lesser-parallel ruft jobs zweimal hinterander auf:
https://github.com/kou1okada/lesser-parallel/blob/master/lesser-parallel#L15-L16
--
....Christian.Garbs....................................https://www.cgarbs.de
"I keep seeing spots before my eyes."
"Have you seen a doctor?"
"No. Just spots.
Tim Landscheidt
2019-11-14 02:27:31 UTC
Permalink
Post by Christian Garbs
[…]
Wenn ich allerdings die mit # ??? markierte Zeile auskommentiere, dann
läuft das Skript endlos weiter und beendet sich nicht.
[…]
Bei mir funktioniert es:

| [***@passepartout ~]$ bash /tmp/test.sh
| 1 start
| 2 start
| 3 start
| 1 stop
| 4 start
| 2 stop
| 5 start
| 3 stop
| 6 start
| 4 stop
| 7 start
| 5 stop
| 8 start
| 6 stop
| 9 start
| 7 stop
| 8 stop
| 9 stop
| finished
| [***@passepartout ~]$

„Läuft das Skript endlos weiter“ oder wartet es vielleicht
auf eine Benutzereingabe in Deinem echten
long_running_command()?

Tim
Christian Garbs
2019-11-14 19:17:21 UTC
Permalink
Mahlzeit!
Post by Tim Landscheidt
Post by Christian Garbs
Wenn ich allerdings die mit # ??? markierte Zeile auskommentiere, dann
läuft das Skript endlos weiter und beendet sich nicht.
[…]
„Läuft das Skript endlos weiter“ oder wartet es vielleicht
auf eine Benutzereingabe in Deinem echten
long_running_command()?
Da wird nicht auf eine Benutzereingabe gewartet, das geht auch in der
hier geposteten Minimalversion zuverlässig kaputt. Die Zeile
" 9 stop" wird auch noch ausgegeben, nur das "finished" kommt nicht mehr.

Mich wundert, dass das bei Dir durchläuft.
Hast Du die mit "# ???" markierte Zeile auch auskommentiert oder gelöscht?

Hier werkelt

| bash --version
| GNU bash, version 4.4.20(1)-release (x86_64-pc-linux-gnu)

aus Ubuntu 18.04 LTS.

Gruß
Christian
--
....Christian.Garbs....................................https://www.cgarbs.de
in the beginning was the word, and the word was content-type: text/plain
Tim Landscheidt
2019-11-16 02:53:43 UTC
Permalink
Post by Christian Garbs
Post by Tim Landscheidt
Post by Christian Garbs
Wenn ich allerdings die mit # ??? markierte Zeile auskommentiere, dann
läuft das Skript endlos weiter und beendet sich nicht.
[…]
„Läuft das Skript endlos weiter“ oder wartet es vielleicht
auf eine Benutzereingabe in Deinem echten
long_running_command()?
Da wird nicht auf eine Benutzereingabe gewartet, das geht auch in der
hier geposteten Minimalversion zuverlässig kaputt. Die Zeile
" 9 stop" wird auch noch ausgegeben, nur das "finished" kommt nicht mehr.
Mich wundert, dass das bei Dir durchläuft.
Hast Du die mit "# ???" markierte Zeile auch auskommentiert oder gelöscht?
[…]
Äh, nein, sorry :-). Ich denke, Stefans Analyse ist korrekt.

Tim
Stefan Reuther
2019-11-14 17:03:46 UTC
Permalink
Post by Christian Garbs
while [ $(jobs | wc -l) -ge $MAX ]; do
jobs >/dev/null # ???
sleep 0.1
done
[...]
Post by Christian Garbs
Warum muss das jobs-Kommando zweimal aufgerufen werden, damit das
Skript funktioniert?
Wenn ich mir da noch mal eine kleine Debugausgabe reinpfriemel...

while [ $(jobs | tee /dev/tty | wc -l) -ge $MAX ]; do

...welche im Fehlerfall wiederholt diese Ausgabe produziert...

[9] Done long_running_command $I

...komm ich zu der Vermutung: man muss ein Kommando wie 'jobs' aufrufen,
damit die Shell überhaupt die beendeten Jobs einsammelt.
Da das jobs-Kommando in einer Pipeline und damit einer Subshell
aufgerufen wird, kann es den Status in der Hauptshell nicht
aktualisieren und der bleibt ewig stehen.

Falls es dir möglich ist, beim Starten eines Jobs etwas zu zählen, würde
ich ja 'wait' benutzen:

----8<----
COUNT=0

wait_max()
{
local MAX=$1
while [ $COUNT -ge $MAX ]; do
wait
COUNT=$((COUNT-1))
done
}

for I in $(seq 9); do
wait_max $MAX_JOBS
long_running_command $I &
COUNT=$((COUNT+1))
done

wait_max 1
echo finished
----8<----


Stefan
Christian Garbs
2019-11-14 19:14:12 UTC
Permalink
Mahlzeit!
Post by Stefan Reuther
Post by Christian Garbs
while [ $(jobs | wc -l) -ge $MAX ]; do
jobs >/dev/null # ???
sleep 0.1
done
[...]
Post by Christian Garbs
Warum muss das jobs-Kommando zweimal aufgerufen werden, damit das
Skript funktioniert?
Wenn ich mir da noch mal eine kleine Debugausgabe reinpfriemel...
while [ $(jobs | tee /dev/tty | wc -l) -ge $MAX ]; do
...welche im Fehlerfall wiederholt diese Ausgabe produziert...
[9] Done long_running_command $I
Das hab ich inzwischen auch ausprobiert - mit dem gleichen Ergebnis.
Vorher hatte ich noch vermutet, ob 'jobs' vielleicht die Pipe irgendwie
mitzählt, aber da war dann klar, dass es was anderes sein muss.
Post by Stefan Reuther
...komm ich zu der Vermutung: man muss ein Kommando wie 'jobs' aufrufen,
damit die Shell überhaupt die beendeten Jobs einsammelt.
Das klingt passend!

Ich hatte in die Richtung überlegt, ob es was mit "Ausgabe geht aufs
Terminal oder nicht" zu tun hat, aber dann blieb die Frage, warum die
Umleitung nach /dev/null das Problem löst.
Post by Stefan Reuther
Da das jobs-Kommando in einer Pipeline und damit einer Subshell
aufgerufen wird, kann es den Status in der Hauptshell nicht
aktualisieren und der bleibt ewig stehen.
Ist das irgendwo dokumentiert?
Ich habe gerade nochmal ohne Ergebnis die bash-Manpage durchforstet.
Post by Stefan Reuther
Falls es dir möglich ist, beim Starten eines Jobs etwas zu zählen,
Das wäre etwas schöner, weil es kein "active wait" ist (alle paar
Millisekunden gucken), aber ist denn sichergestellt, dass jedes 'wait'
nur exakt einen fertigen Job abräumt?

Bis nach dem ersten 'wait' die Schleife durchlaufen und die Shell
wieder beim nächsten 'wait' angekommen ist, könnten ja mehrere Jobs
fertig geworden sein. Wenn ich dann den COUNTER nur um 1 reduziere,
gibt es Probleme.



Was übrigens auch geht: 'jobs -r' listet nur die laufenden Jobs.
Dann kommt niemals eine "Done"-Zeile und man kann sich den zweiten
Aufruf von 'jobs' sparen.

Bei der Variante hätte ich dann aber Angst, dass mal ein Job ein
SIGSTOP bekommt - und dann wird ein neuer Job nachgestartet, weil
'jobs -r' den gestoppten Job ja nicht mitzählt…



Alles in allem bin ich mit der funktionierenden Ursprungsvariante mit
dem doppelten 'jobs'-Aufruf erstmal glücklich.
Ich wollte ja nur wissen, warum der doppelte Aufruf nötig ist :-)


Vielen Dank!
Christian
--
....Christian.Garbs....................................https://www.cgarbs.de
In der Theorie ist Theorie und Praxis dassselbe.
In der Praxis nicht.
Stefan Reuther
2019-11-15 16:59:56 UTC
Permalink
Post by Christian Garbs
Post by Stefan Reuther
Da das jobs-Kommando in einer Pipeline und damit einer Subshell
aufgerufen wird, kann es den Status in der Hauptshell nicht
aktualisieren und der bleibt ewig stehen.
Ist das irgendwo dokumentiert?
Ich habe gerade nochmal ohne Ergebnis die bash-Manpage durchforstet.
Geht mir genauso. Ich hatte auch mit 'set -b' experimentiert ("Notify of
job termination immediately."), ohne Erfolg.
Post by Christian Garbs
Post by Stefan Reuther
Falls es dir möglich ist, beim Starten eines Jobs etwas zu zählen,
Das wäre etwas schöner, weil es kein "active wait" ist (alle paar
Millisekunden gucken), aber ist denn sichergestellt, dass jedes 'wait'
nur exakt einen fertigen Job abräumt?
Bis nach dem ersten 'wait' die Schleife durchlaufen und die Shell
wieder beim nächsten 'wait' angekommen ist, könnten ja mehrere Jobs
fertig geworden sein. Wenn ich dann den COUNTER nur um 1 reduziere,
gibt es Probleme.
Stimmt. Die Beschreibung von 'wait' klingt so, als ob man die Option
'-n' braucht ("If the -n option is supplied, wait waits for any job to
terminate and returns its exit status."). Ich hab nur extrapoliert, was
ich in C machen würde, und das mal stichprobenartig probiert.

Die Lösung mit 'jobs' ist sicher robuster, auch gegen Vergessen des
Hochzählens.


Stefan
Christoph 'Mehdorn' Weber
2019-11-19 18:36:01 UTC
Permalink
Hallo!
Post by Christian Garbs
Ich möchte mehrere Kommandos in begrenzter Menge parallel laufen
lassen
(*) Eigentlich wollte ich GNU Parallel benutzen, aber in dem
Container, wo das Skript laufen soll, gibt es kein Perl.
Als ich "parallel" noch nicht kannte (oder es das noch nicht
gab), hab ich immer "xargs --max-procs" benutzt. Vielleicht geht
das einfacher. Koennte aber ein GNU voraussetzen, was vielleicht
ein Problem ist, wenn es nicht mal Perl gibt.

Christoph
--
Bei uns wird Hand in Hand gearbeitet:
Was die eine nicht schafft, laesst sie andere liegen.
Christian Garbs
2019-11-19 23:40:50 UTC
Permalink
Mahlzeit!
Post by Christoph 'Mehdorn' Weber
Als ich "parallel" noch nicht kannte (oder es das noch nicht
gab), hab ich immer "xargs --max-procs" benutzt. Vielleicht geht
das einfacher. Koennte aber ein GNU voraussetzen, was vielleicht
ein Problem ist, wenn es nicht mal Perl gibt.
Das wäre in Kombination mit --max-args tatsächlich möglich gewesen,
aber dann hätte ich (wie bei parallel auch) zwei Shellskripte gehabt:
Eines, das die Datensätze zusammensucht (und noch etwas mehr tut) und
eines, das einen einzelnen Datensatz verarbeitet.

So wie es jetzt ist, habe ich nur ein einziges Skript, in dem beides
drin ist, das ist in diesem Fall übersichtlicher. Die ca. 10 Zeilen
Shellcode für die Parallelisierung nehme ich dafür gerne in Kauf.

(ob es xargs gibt oder es einfach nachinstallierbar ist, müsste ich
erst nachschauen)

Gruß
Christian
--
....Christian.Garbs....................................https://www.cgarbs.de
There's no real need to do housework --
after four years it doesn't get any worse.
Stefan Reuther
2019-11-20 10:00:11 UTC
Permalink
Post by Christoph 'Mehdorn' Weber
Post by Christian Garbs
Ich möchte mehrere Kommandos in begrenzter Menge parallel laufen
lassen
(*) Eigentlich wollte ich GNU Parallel benutzen, aber in dem
Container, wo das Skript laufen soll, gibt es kein Perl.
Als ich "parallel" noch nicht kannte (oder es das noch nicht
gab), hab ich immer "xargs --max-procs" benutzt. Vielleicht geht
das einfacher. Koennte aber ein GNU voraussetzen, was vielleicht
ein Problem ist, wenn es nicht mal Perl gibt.
Wenn es ganz allgemein um Parallelisierung mit ein wenig Logik geht,
nehme ich normalerweise 'make' (mit einem ggf. generierten Makefile).
Damit lässt sich problemlos "maximal N Prozesse" ausdrücken, aber auch
"diese Aufgabe erst nach jenen Aufgaben ausführen".


Stefan
Celal Dikici
2020-02-06 13:53:42 UTC
Permalink
Post by Stefan Reuther
Wenn es ganz allgemein um Parallelisierung mit ein wenig Logik geht,
nehme ich normalerweise 'make' (mit einem ggf. generierten Makefile).
Damit lässt sich problemlos "maximal N Prozesse" ausdrücken, aber auch
"diese Aufgabe erst nach jenen Aufgaben ausführen".
hättest du da mal ein Beispiel-Template?

Ich erstelle hier diverse SQL files (on-the-fly), die dann ans isql übergeben werden. Bei einem ähnlichen Fall habe ich GNU Parallel benutzt und es klappt gut damit. Jetzt aber kann ich nicht alle SQL files übergeben sondern muss eine *Reihenfolge* beachten.
Ich habe das aktuell mit Christians Lösung gemacht; leider aber noch nicht ganz richtig & sauber bin aber noch dabei.
Ich würde jedoch auch gerne das von dir vorgeschlagene 'make' Version mal testen. Da ich sonst nie was mit make/makefiles hantiere, würde mir eine Vorlage hilfreich sein.

Danke im Voraus,
Celal
Stefan Reuther
2020-02-06 17:04:46 UTC
Permalink
Post by Celal Dikici
Post by Stefan Reuther
Wenn es ganz allgemein um Parallelisierung mit ein wenig Logik geht,
nehme ich normalerweise 'make' (mit einem ggf. generierten Makefile).
Damit lässt sich problemlos "maximal N Prozesse" ausdrücken, aber auch
"diese Aufgabe erst nach jenen Aufgaben ausführen".
hättest du da mal ein Beispiel-Template?
Na ein Makefile eben :)
Post by Celal Dikici
Ich erstelle hier diverse SQL files (on-the-fly), die dann ans isql
übergeben werden. Bei einem ähnlichen Fall habe ich GNU Parallel
benutzt und es klappt gut damit. Jetzt aber kann ich nicht alle SQL
files übergeben sondern muss eine *Reihenfolge* beachten.
Bei SQL kann ich nicht mitreden. Aber "generiere aus einem Schwung
Eingabedateien ein paar Zwischendateien ('erstelle SQL files'), und
verarbeite die zu einem Gesamtergebnis weiter ('ans isql übergeben')"
ist quasi der typische Anwendungsfall für make zum Bauen eines Programms:

# Zusammenbau
programm: one.o two.o three.o
gcc -o programm one.o two.o three.o

# Zwischenschritte
one.o: one.c
gcc -o one.o one.c
two.o: two.c
gcc -o two.o two.c
three.o: three.c
gcc -o three.o three.c

Mit 'make -j2' werden hier zwei der ".c -> .o" Schritte parallel
ausgeführt wenn möglich.

Das kann man jetzt noch mit Pattern-Regeln, Variablen usw. verfeinern,
um die Redundanz zu entfernen, oder eben generieren.


Stefan

Loading...