angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Python file I/O auf Windows und Linux sind zwei verschiedene Dinge

Bei der Verwendung von Threads kann es zu Problemen kommen, die durch das unterschiedliche Verhalten von Datei-E/A-Funktionen verursacht werden.

8 Dezember 2021
In Python
post main image
https://unsplash.com/@momofactory

Ich habe ein Python -Programm, das problemlos auf Linux läuft. Vor ein paar Monaten wollte ich es auf Windows laufen lassen.

Dies war das erste Mal, dass ich Python auf Windows verwendet habe. Python App installieren, virtual environment erstellen, kopieren und ausführen. Keine Probleme ... oh, aber es gab ein Problem. Meine Sitzung verschwand manchmal ... WTF! Ich bemerkte das Problem durch wiederholtes Drücken von F5 in sehr kurzer Zeit. Zeit für eine gründliche Untersuchung.

Die Anwendung und der Sitzungs-Schlüsselwertspeicher

Die Anwendung Python ist eine Flask -Anwendung. Sie verwendet die Dateisystemschnittstelle von Flask-Session, um Sessions als Dateien zu speichern. Flask-Session verwendet ein anderes PyPi-Paket, um alle Datei-E/A zu behandeln. Wie Sie sich vorstellen können, implementiert dieses Paket einen Key-Value-Speicher.

Es gibt zwei Hauptmethoden in diesem Paket:

  • set(key, value), zum Schreiben/Aktualisieren der Sitzungsdaten
  • get(key), zum Abrufen der Sitzungsdaten

Im Paket erstellt die Methode set(key, value) eine temporäre Datei und ruft dann die Funktion Python os.replace() auf, um die Session-Datei zu ersetzen. Die Methode get(key) ruft die Lesefunktion Python auf.

Test mit Threads

Wenn Sie Flask im Entwicklungsmodus ausführen, werden Sie viele Anfragen an den Sitzungsspeicher sehen, da Flask auch images, CSS-Dateien usw. bereitstellt. In einer Produktionsumgebung, in der Sie statische Inhalte über einen Webserver bereitstellen, ist dieses Problem zwar weniger wahrscheinlich, aber es besteht trotzdem!
So kam ich auf die Idee, einen kleinen Test mit Threads zu schreiben.

import cachelib
import threading

fsc = cachelib.file.FileSystemCache('.')

def set_get(i):
    fsc.set('key', 'val')
    val = fsc.get('key')

for i in range(10):
    t = threading.Thread(target=set_get, args=(i,))
    t.start()

Auf Linux gab es keine Fehler, nichts. Aber auf Windows führte dies zu zufällig generierten Ausnahmen:

[WinError 5] Access is denied
...
[Errno 13] Permission denied

Die Ausnahme [WinError 5] wurde von der Funktion Python os.replace() erzeugt. Die [Errno 13]-Ausnahme wurde von der Funktion Python read() erzeugt.

Was ist hier los?

File I/O auf Windows und Linux sind zwei verschiedene Dinge

Ich nahm an, dass Python mich vor plattformspezifischen Implementierungen schützen würde. Das ist bei vielen Funktionen der Fall, aber nicht bei allen. Insbesondere bei der Verwendung von Threads kann es zu Problemen kommen, die durch unterschiedliches Verhalten von Datei-E/A-Funktionen verursacht werden.

Aus Python Bug Tracker Issue46003:

Wie man so schön sagt, gibt es keine 'portable Software', sondern nur 'Software, die portiert wurde'.
Besonders in einem Bereich wie Datei-E/A: Sobald man über das einfache 'ein Prozess öffnet, schreibt und schließt' und ein anderer Prozess dann 'öffnet, liest und schließt' hinausgeht, gibt es eine Menge plattformspezifischer Probleme. Python versucht nicht, alle möglichen Datei-I/O-Probleme weg zu abstrahieren.

Python read()-Funktion

In der Bibliothek, die ich verwende, verwendet die Methode get(key) die Funktion Python read().

Bei Linux genügt es, die read()-Funktion in ein try-except zu setzen:

    try:
        with open(f, 'r') as fo:
            return fo.read()
    except Exception as e:
        return None

Die Funktion wartet dann, bis die Daten verfügbar sind. Eine Ausnahme wird nur dann ausgelöst, wenn ein Timeout auftritt, der auf den meisten Linux -Systemen 60 Sekunden beträgt, oder ein anderer unerwarteter Fehler auftritt.

Auf Windows wird dies sofort fehlschlagen, wenn ein anderer Thread auf die Datei zugreift. Um das gleiche Verhalten wie bei Linux zu erreichen, müssen wir Wiederholungen und eine Verzögerung hinzufügen:

    max_sleep_time = 10
    total_sleep_time = 0
    sleep_time = 0.02
    while total_sleep_time < max_sleep_time:
        try:
            with open(f, 'r') as fo:
                return fo.read()
        except OSError as e:
            errno = getattr(e, 'errno', None)
            if errno == 13:
                # permission error
                time.sleep(sleep_time)
                total_sleep_time += sleep_time
                sleep_time *= 2
            else:
                # some other error             
                return None
        except Exception as e:
            return None

    # out of retries
    return None

Python os.replace() Funktion

In der Bibliothek, die ich verwende, verwendet die Methode set(key, value) die Funktion Python os.replace().

Bei Linux reicht es aus, die os.replace()-Funktion in ein try-except zu setzen:

    try:
        os.replace(src, dst)
        return True
    except Exception as e:
        return False

Die Funktion wird warten, bis die Datei ersetzt werden kann. Eine Ausnahme wird nur dann ausgelöst, wenn eine Zeitüberschreitung auftritt, die auf den meisten Linux -Systemen 60 Sekunden beträgt, oder ein anderer unerwarteter Fehler auftritt.

Auf Windows wird dies sofort fehlschlagen, wenn ein anderer Thread auf die Datei zugreift. Um das gleiche Verhalten wie bei Linux zu erreichen, müssen wir z. B. Wiederholungen und eine Verzögerung hinzufügen:

    max_sleep_time = 10
    total_sleep_time = 0
    sleep_time = 0.02
    while total_sleep_time < max_sleep_time:
        try:
            os.replace(src, dst)
            return True
        except Exception as e:
            winerror = getattr(e, 'winerror', None)
            if winerror == 5:
                time.sleep(sleep_time)
                total_sleep_time += sleep_time
                sleep_time *= 2
            else:
                # some other error
                return False

    # out of retries
    return False

Schlussfolgerung

Die Erstellung eines Python -Programms, das auf mehreren Plattformen laufen kann, kann kompliziert sein, da man auf Probleme wie die oben beschriebenen stoßen kann. Zuerst war ich überrascht, dass Python die Komplexität von Windows nicht vor mir versteckt hat. Da ich von Linux komme, dachte ich: Python , warum funktioniert das nicht auf Windows so, wie es auf Linux funktioniert?
Aber das ist die Entscheidung, die die Python -Entwickler getroffen haben. Vielleicht ist es auch nicht möglich. Ich konnte online keine einzige Zeile in den Python -Dokumenten finden, die mich darauf aufmerksam gemacht hätte, und habe festgestellt, dass viele Leute mit diesem Problem zu kämpfen haben. Ich reichte einen Fehlerbericht ein und bat darum, dass eine Warnung für Entwickler hinzugefügt wird, wenn sie für mehrere Plattformen entwickeln. Später habe ich davon Abstand genommen, weil ich weiß, dass ich sehr voreingenommen bin, wenn ich von Linux komme.

Links / Impressum

backports.py
https://github.com/flennerhag/mlens/blob/master/mlens/externals/joblib/backports.py

os.replace
https://docs.python.org/3/library/os.html?highlight=os%20replace#os.replace

os.replace is not cross-platform: at least improve documentation
https://bugs.python.org/issue46003

Mehr erfahren

Threads

Einen Kommentar hinterlassen

Kommentieren Sie anonym oder melden Sie sich zum Kommentieren an.

Kommentare

Eine Antwort hinterlassen

Antworten Sie anonym oder melden Sie sich an, um zu antworten.