Seite 1 von 1

Popen hängt

Verfasst: Donnerstag 3. Juni 2004, 18:35
von Leonidas
Hallo Leute!
Ich habe folgendes Problem: ich möchte eine Datei namens ger-eng.txt (ja, ein 6MB großes wörterbuch) mit agrep durchsuchen. Nun, da agrep nicht zum Windows Standardumfang gehört, habe ich mir irgendwo ein Win32 binary heruntergeladen, keine Ahnung wo (denke es waren die UnxUtils).
Ihr könnt es aber hier
runterladen. Das Wörterbuch ist hier
zu finden. Der aufruf ist

Code: Alles auswählen

agrep -ihw 'get' ger-eng.txt
Das mache ich auch mit os.popen3:

Code: Alles auswählen

import os
stdin, stdout, stderr = os.popen3("agrep -ihw 'get' ger-eng.txt")
if stderr.readlines() != []:
    print stdout.readlines()
Das hängt! Aber wenn ich erst stdout.readlines() und dann stderr.readlines() dann ist alles in ordnung (aber ich will ja erst wissen ob es Sinn macht von stdout zu lesen, denn wenn in stderr was zu finden ist, kann man es gleich vergessen). Interessanterweise läuft es beim aufruf von agrep mit abc statt get problemlos.
Hilfe!

Verfasst: Freitag 4. Juni 2004, 19:42
von Milan
Hi. Bist du ganz sicher, dass es nicht eher eine Abfrage auf Gleichheit, statt auf Ungleichheit (!=) sein soll :wink: ? Ansonsten... mit der kleinen Korrektur läuft es wunderbar: er findet keine passenden Wörter (wird wohl an -w liegen), aber meldet das auch korrekt.

Verfasst: Montag 7. Juni 2004, 07:23
von Leonidas
Milan hat geschrieben:Hi. Bist du ganz sicher, dass es nicht eher eine Abfrage auf Gleichheit, statt auf Ungleichheit (!=) sein soll :wink: ? Ansonsten... mit der kleinen Korrektur läuft es wunderbar: er findet keine passenden Wörter (wird wohl an -w liegen), aber meldet das auch korrekt.
Ja, sollte eigentlich == sein, stimmt. Also bei mir hängt es. Ich habe es jetzt so gelöst: zuerst lese ich stdout aus, dann stderr und wenn dort was drinsteht, dann wird stdout verworfen. Naja, auch gut.

Re: Popen hängt

Verfasst: Samstag 2. November 2024, 19:05
von __blackjack__
Hier wurde die Ursache nicht geklärt und `os.popen3()` gibt es mittlerweile nicht mehr. Und tatsächlich wurde der Fehler auch gar nicht behoben, denn auch beim umgekehrten auslesen der beiden Dateien, ist nicht garantiert, dass das problemlos funktioniert.

Erst einmal kann ich das konkrete Beispiel selbst mit Python 2.7 nicht mehr nachvollziehen. Das ist nämlich so ein typischer Fehler bei Nebenläufigkeit, wo es von der Umgebung und den konkreten Daten(mengen) abhängt, ob das zufällig noch funktioniert, oder man in eine Verklemmung („deadlock“) läuft und alles hängt, bis man einen der beiden Prozesse von aussen beendet.

Wenn man beide Ausgaben eines externen Prozesses per Pipe abgreift, dann darf man die nicht nacheinander blockierend auslesen, sondern muss die ”gleichzeitig” lesen. Also nicht-blockierende Leseoperationen verwenden, oder so etwas wie `select.select()` um festzustellen bei welche(r|n) Datei(en) gerade Daten anliegen, oder mit einem Thread pro Datei.

Zwischen den beiden Prozessen gibt es nämlich einen Puffer für jede Pipe und wenn der Puffer voll ist, wartet der schreibende Prozess solange bis der lesende Prozess Daten ausgelesen hat und wieder Platz im Puffer ist. Hier wartet das Python-Programm im ersten Beitrag solange auf Ausgaben von ``agrep`` auf `stderr` bis ``agrep`` fertig ist und sein Ende der Pipe schliesst. ``agrep`` hingegen schreibt das Ergebnis der Suche in `stdout`. Der Puffer dort ist dann irgendwann voll und ``agrep`` wartet bis das Python-Programm anfängt davon zu lesen. Und in diesem Zustand warten die beiden Programme dann bis in alle Ewigkeit aufeinander.

Wenn man im Python-Programm erst `stdout` ausliest und dann `stderr` scheint der Fehler behoben, aber es kann auch bei *der* Reihenfolge grundsätzlich das gleiche Problem auftreten. Das ist also keine wirkliche Lösung.

Vor 20 Jahren war der Puffer für Pipes bei Linux 4 KiB gross. Heute ist der grösser und zwar gross genug, dass bei dem konkreten Beispiel aus dem ersten Beitrag scheinbar kein Problem existiert, denn das Ergebnis von dem ``agrep``-Aufruf passt komplett in den Pufferspeicher.

Die Grösse des Pufferspeichers von Pipes kann man ”live” und ziemlich low-level ermitteln, denn Python hat dünne Wrapper über die C-Funktionen um Pipes zu erstellen und davon dann die Grösse abzufragen:

Code: Alles auswählen

In [321]: a, b = os.pipe()

In [322]: fcntl.fcntl(a, fcntl.F_GETPIPE_SZ)
Out[322]: 65536

In [323]: os.close(a); os.close(b)

In [324]: 65536 / 1024
Out[324]: 64.0
Auf aktuellen Linux-Systemen ist der Puffer also nicht mehr 4 KiB sondern 64 KiB gross, was für das komplette Ergebnis von dem ``agrep``-Aufruf locker ausreicht, denn der belegt nicht einmal die Hälfte des Pufferspeichers:

Code: Alles auswählen

$ agrep -ihw 'get' Downloads/ger-eng.txt | wc --bytes
30103
Eine erste Näherung des (fehlerhaften) Programms in Python 3 mit dem `subprocess`-Modul könnte so aussehen:

Code: Alles auswählen

#!/usr/bin/env python3
from subprocess import PIPE, Popen


def main():
    with Popen(
        "agrep -ihw 'get' Downloads/ger-eng.txt",
        shell=True,
        stdin=PIPE,
        stdout=PIPE,
        stderr=PIPE,
    ) as agrep:
        if agrep.stderr.readlines() == []:
            print(agrep.stdout.readlines())


if __name__ == "__main__":
    main()
Jetzt brauchen wir aber weder die zusätzliche Shell noch `stdin` wirklich. Dafür möchte man vielleicht gerne Zeichenketten statt Bytes haben, also muss man die Kodierung angeben:

Code: Alles auswählen

#!/usr/bin/env python3
from subprocess import PIPE, Popen
from threading import Thread


def main():
    with Popen(
        ["agrep", "-ihw", "get", "Downloads/ger-eng.txt"],
        stdout=PIPE,
        stderr=PIPE,
        encoding="iso-8859-1",
    ) as agrep:
        error_lines = []
        thread = Thread(target=lambda: error_lines.extend(agrep.stderr))
        thread.start()
        lines = list(agrep.stdout)
        thread.join()

    if not error_lines:
        print(lines)


if __name__ == "__main__":
    main()
Wenn es okay ist, dass man die Ausgabe als *eine* Zeichenkette bekommt, statt als Liste mit Zeilen, kann man es sich mit `subprocess.run()` einfacher machen:

Code: Alles auswählen

#!/usr/bin/env python3
from subprocess import run


def main():
    result = run(
        ["agrep", "-ihw", "get", "Downloads/ger-eng.txt"],
        capture_output=True,
        encoding="iso-8859-1",
        check=False,
    )
    if not result.stderr:
        print(result.stdout.splitlines(keepends=True))


if __name__ == "__main__":
    main()