subprocess: Unterprozess starten, um ein Speicherlimit zu lösen
Ziel
Ich kann Teilaufgaben an einen Unterprozess delegieren, um tolle Fähigkeiten existierender Nicht-Python-Software zu nutzen, anstatt in Python großen Aufwand zu treiben.
Hintergrund
Manchmal scheitert ein naiv geschriebenes Pythonprogramm katastrophal an einer Leistungsanforderung, die ein gekonnt geschriebenes mit nur wenigen zusätzlichen Programmzeilen lässig hinbekommt. So ein Beispiel turnen wir in dieser Aufgabe durch. Nicht ganz einfach, aber mit hohem Fun-Faktor.
Das Beispiel basiert darauf, dass man den Median einer Sequenz von Werten am einfachsten so ausrechnet, dass man die Sequenz sortiert und dann das mittlere Element herausholt. Doch was, wenn die Sequenz so lang ist, dass sie nicht in den Speicher passt?
Arbeitsschritte
Falls Sie sich noch nie näher mit Debugging beschäftigt haben, empfiehlt es sich, sich darin vor dieser Aufgabe ein Rüstzeug zuzulegen.
Programm, Teil 1
- 1 Legen Sie das Programm
m_subprocess2.py
an und realisieren Sie darin die folgenden Funktionen: - 2
initrandom()
initialisiert den Zufallsgenerator mit dem Wert 42, damit jeder Programmlauf dasselbe Ergebnis bringt. - 3
random_line() -> str
erzeugt eine Zeile (mit"\n"
am Ende) die in Hexadezimalschreibweise die SHA512-Prüfsumme von 4 mit einem einzigenrandom
-Aufruf erzeugten Zufallsbytes enthält. - 4
random_lines_generator(n: int) -> collections.abc.Iterator[str]:
ist eine Generator-Funktion, deren Iteratorn
solcherandom_line()
-Zeilen liefert. - 5
sorted_lines_generator_python(n: int) -> collections.abc.Iterator[str]:
ist eine darauf aufbauende Generator-Funktion, deren Iteratorn
solche Zeilen liefert, die mittelssorted
sortiert wurden. - 6
median(numlines: int, sorted_lines_iterator: collections.abc.Iterator[str]) -> str:
berechnet den Median vonnumlines
Zeilen, die vonsorted_lines_iterator
bereits in sortierter Reihenfolge bereitgestellt werden. Bei einer ungeraden Zahl von Zeilen ist das die mittlere Zeile, bei einer geraden Zahl ist es die Zeile nach der Mittellücke.
Tests zu Teil 1
Schreiben Sie nun ein paar zugehörige Unittests direkt mit in die gleiche Datei:
- 7
test_random_line():
stellt sicher, dass die erste erzeugte Zufallszeile auf"2ed9f9\n""
endet. - 8
test_random_lines():
stellt sicher, dass bei 4 mittelsrandom_lines_generator
erzeugten Zufallszeilen die erste mit"6e9a55""
beginnt und die letzte mit"0f757a\n""
endet. - 9
def test_median4():
stellt sicher, dass der Median von 4 Zufallszeilen mit"72f436"
beginnt und mit"d68c77\n"
endet. - 1 Zeigen sie einen erfolgreichen Lauf von
pytest -v m_subprocess2.py
.
Hauptprogramm dazu
- 10 Ergänzen Sie unten in derselben Datei ein Hauptprogramm, das man mit 2 Kommandozeilenargumenten
aufruft: Argument 1 ist entweder "local" oder "print";
Argument 2 ist eine ganze Zahl
n
.
Bei "local" gibt das Programm das Ergebnis vonmedian(n, sorted_lines_generator_python(n))
aus.
Bei "print" gibt es die Ergebnisse vonrandom_lines_generator(n)
aus. - 2 Zeigen sie
python m_subprocess2.py print 4
. - 3 Zeigen sie
python m_subprocess2.py local 4
.
Speicherbeschränkung
Nun kommen wir zum Thema. Damit wir nicht Ewigkeiten warten müssen, bis der Speicher eines modernen Rechners wirklich ganz gefüllt ist, machen wir uns eine Speicherbeschränkung auf 512 Megabyte künstlich.
- 4 Zeigen sie
python m_subprocess2.py local 5000000
. - Lesen Sie sich das bash-Kommando
ulimit
an. - 5 Zeigen Sie ein Kommando, das mittels
ulimit
den virtuellen Speicher auf 512.000 Kilobytes beschränkt und dannpython m_subprocess2.py local 5000000
aufruft.
Dieser Aufruf sollte mit "MemoryError" fehlschlagen, denn diese Daten passen nicht mehr alle in die 512 MB hinein. (Diese Fehlschläge beginnen irgendwo zwischen 2000000 und 3000000. Der genaue Wert hängt von vielen Einzelheiten ab.)
Programm, Teil 2
Nun bauen wir eine Variante die trotz der Speicherbeschränkung auf 512 MB noch immer 5 Millionen Zeilen sortieren kann.
- 11 Ergänzen Sie Ihr Programm um folgende Funktion:
sorted_lines_generator_subprocess(n: int) -> collections.abc.Iterator[str]:
Diese hat von außen gesehen die gleiche Funktionalität wiesorted_lines_generator_python
, aber sie benutzt zum Sortieren das Modulsubprocess
, um das Unix-Utilitysort
aufzurufen.
Sie schreibt dabei die Zeilen direkt (also ohne Zwischendatei) über eine Pipe in densort
-Prozess und liest die sortierten Ergebnisse ebenfalls direkt, also wieder ohne Zwischendatei, sondern ebenfalls über eine Pipe aus demsort
-Prozess. - 12 Ergänzen Sie Ihr Hauptprogramm so, dass das erste Argument auch "subprocess" lauten darf
und dann
sorted_lines_generator_subprocess
stattsorted_lines_generator_python
verwendet wird.
Hinweis (nur bei Bedarf): Debugginghilfe
Wenn man sich vertut, wird die Problemsuche schnell unübersichtlich,
wenn man mit Unterprozessen arbeitet.
Folgende Testausgaben sind vermutlich hilfreich, jedenfalls wenn Sie sie nach sys.stderr
schreiben:
- in
random_lines_generator
summieren Sie die Länge der erzeugten Zeilen auf und machen jeweils nach 200000 Ergebnissen eine Testausgabe der Form
line 200000: 24.6 MB total output
- in
median
machen Sie jeweils nach 200000 Ergebnissen eine Testausgabe der Form
have read 200k lines
Tests zu Teil 2
- 13 Ändern Sie den existierenden Test
def test_median4()
auf folgende Signatur ab:
1 2 |
|
- Das soll dieselbe bisherige Testlogik mit den angegebenen zwei verschiedenen Iteratoren ausführen, wobei jede der Varianten separat fehlschlagen kann und selbständig berichtet wird.
- 6 Zeigen sie einen erfolgreichen Lauf von
pytest -v m_subprocess2.py
. - 14 Lesen Sie den obigen Hinweis und ergänzen Sie die Debuggingausgaben genau wie dort beschrieben, um die Kontrolle Ihres Ergebnisses zu erleichtern.
Voilá!
- 7 Zeigen Sie ein Kommando, das mittels
ulimit
den virtuellen Speicher auf 512.000 Kilobytes beschränkt und dannpython m_subprocess2.py subprocess 5000000
aufruft.
Dieser Aufruf sollte erfolgreich sein und das korrekte Resultat "800aea48b0…" liefern, obwohl diese Daten nicht mehr alle in die 512 MB hineinpassen.
Erstaunlich, oder?
- 1 Aber warum funktioniert das?
Der Unterprozess unterliegt doch derselben Speicherbeschränkung auf 512 MB,
in der unsere Daten nicht genug Platz finden!
Recherchieren Sie, wie GNU sort das anstellt, und geben Sie für die Antwort eine möglichst vertrauenswürdige Quelle an.
Abgabe
Geben Sie den Quellcode ab, wie er am Ende der Aufgabe vorliegt.
Geben Sie ein Kommandoprotokoll ab, das genau nur die Eingaben und Ausgaben der obigen Kommandos 1, 2, … enthält. Entfernen Sie vor Abgabe eventuelle Fehlversuche und sonstige zusätzliche Kommandos aus dem Protokoll.
Geben Sie ein Markdown-Dokument ab mit knappen Antworten zu den oben gestellten Fragen
1, 2, … Geben Sie diese Marker mit an.
Geben Sie ggf. Beispiele oder benutzte Quellen an.