Gleichzeitigen Dateizugriff sicher verhindern

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Rakanischu
User
Beiträge: 3
Registriert: Freitag 25. Juli 2008, 20:28
Kontaktdaten:

Freitag 25. Juli 2008, 21:15

Hallo.

Ich beschäftige mich seit ein paar Monaten mit Python und konnte bis dato alle auftauchenden Fragen mittels Google & Co. auch ohne eigene Thread-Eröffnung lösen. Jetzt allerdings ist anscheinend der Zeitpunkt gekommen, wo ich mit meinem Latein endgültig erstmal am Ende bin.

Mein derzeitiges Projekt ist ein kleines ( also nicht öffentliches, sondern nur für den Freundeskreis ) Browserspiel ( nicht, dass es davon nicht schon reichlich Auswahl gäbe, aber selber machen ist auch mal was ). Soll dann auch nicht auf einem root laufen ( wäre wohl gelinde gesagt ein leichter Overkill, außerdem wäre das Anschaffen eines Rootservers bei meiner derzeitigen Kenntnislage wohl grob fahrlässig ), sondern ein Kumpel hat nicht-kostenlosen Webspace, auf dem Python lauffähig ist.

Ich habe - vielleicht jetzt rückblickend gesehen ein Fehler - mich dafür entschieden, nicht auf "vorgefertigte" Module für mySQL etc. zurückzugreifen, sondern ein eigenes Modul anzufertigen, dass die nötigen Such/Lese/Schreib-Funktionen für den letztlich wohl eher übersichtlichen Datenbestand beinhaltet.

Für eine Tabelle ( z.B. mit den Datensätzen für die Accounts ) wird eine entsprechende Datei angelegt. Um nun zu verhindern, dass verschiedene Zugriffe zur gleichen Zeit auf dieselbe Tabelle stattfinden, soll das erste Byte Auskunft darüber geben, ob die Daten gerade in Benutzung sind oder nicht, also nach dem Schema
"0": frei
"1": belegt, nicht stören
Dieses Byte wird immer erst geprüft und gegebenenfalls etwas gewartet, ehe ein erneuter Zugriffsversuch erfolgt.

Code: Alles auswählen

Pseudocode:

Funktion Lesen(parameter):

    tabelle = open(tabellenname, "rb+")
    Solange [erstes Byte der Datei als ISO 8859-1] == "1": #prüfe, ob Datei "frei ist"
        ein kleines bisschen Abwarten
    ansonsten:
        erstes Byte auf "1" schreiben #Datei als besetzt markieren
        Anfrage abarbeiten
        erstes Byte wieder auf "0" schreiben #fertig
Funktioniert soweit auch, bis auf den Sonderfall, dass zwei verschiedene Funktionsaufrufe, die dieselbe Tabelle betreffen, so dermaßen knapp hintereinander eintreffen, dass ( so vermute ich ) folgendes passiert:
Instanz 1 wird nach der if-Bedingung in Zeile 6, aber noch vor dem Setzen des Bytes auf "1" kurzfristig schlafen gelegt. Bevor es weitergeht, läuft Instanz 2 mindestens genauso weit.
Jetzt meinen beide Instanzen, dass die Datei frei ist, und legen quasi parallel los. Die Folgen reichen von "gar nichts passiert" bis "alles zerschossen". Das jedenfalls haben simple Versuche meinerseits bestätigt.

Nun kam mir das Stichwort "thread-sicher" in den Sinn, also Google etc. angeschmissen und gesucht. Ergebnis: Das grundlegende Problem ist zwar weit bekannt, allerdings beziehen sich die Lösungsansätze ( "mutexe", "semaphoren" etc. ) anscheinend immer auf den Fall, dass innerhalb einer ausgeführten "Instanz eines Scripts" Multithreading betrieben werden soll, nicht dass mehrere Instanzen auf dieselben Daten zugreifen wollen ( mit "Instanzen eines Scripts" meine ich, wenn man ganz doof ausgedrückt auf eine .py doppelklickt und das mehrmals hintereinander, noch bevor das Erste fertig ist ).

Ich könnte ja jetzt sagen "gut, dann mache ich das eben doch mit SQL und werfe meine bisherige Arbeit weg", aber das hat was davon, das Problem unter den Teppich zu kehren, statt es zu lösen...

Als allererstes das Byte auf "1" setzen, damit die Datei in jedem Fall auf "belegt" steht, bringt ja nichts. Wenn ich es überschreibe, ohne es je gelesen zu haben, werde ich auch selbst nie erfahren, auf was es denn ursprünglich gestanden hat und ob ich nun weiter verfahren darf oder nicht.
Benutzeravatar
gerold
Python-Forum Veteran
Beiträge: 5555
Registriert: Samstag 28. Februar 2004, 22:04
Wohnort: Oberhofen im Inntal (Tirol)
Kontaktdaten:

Freitag 25. Juli 2008, 21:28

Hallo Rakanischu!

Willkommen im Python-Forum! :-)

Vielleicht kannst du damit etwas anfangen:
http://www.python-forum.de/post-100888.html#100888

mfg
Gerold
:-)
http://halvar.at | Kleiner Bascom AVR Kurs
Wissen hat eine wunderbare Eigenschaft: Es verdoppelt sich, wenn man es teilt.
Rakanischu
User
Beiträge: 3
Registriert: Freitag 25. Juli 2008, 20:28
Kontaktdaten:

Samstag 26. Juli 2008, 00:03

Danke! Das scheint geholfen zu haben.

Code: Alles auswählen

class Lockfile():

    def __init__(self, tablename):
        self.tablename = tablename
        self.success = False

    def Versuchen(self):
        try:
            self.filedesc = os.open("../db/"+self.tablename+".lck", os.O_CREAT+os.O_EXCL)
            self.success = True
            return True
        except OSError:
            return False

    def Loeschen(self):
        if self.success:
                os.close(self.filedesc)
                os.remove("../db/"+self.tablename+".lck")



def Read(tablename, key, hint): #lese alle Einträge mit Schlüssel [key] oder nutze Positionshinweis [hint]

    lock = Lockfile(tablename)

    while lock.Versuchen() == False:
        time.sleep(TIME_TO_RETRY)
    else:
        [Abarbeitung der Anfrage]
        lock.Loeschen()
        del lock
funktioniert. Jetzt kann ich endlich wieder ruhiger schlafen ;-)
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Samstag 26. Juli 2008, 08:50

Browserspiel lässt mich aufhorchen. Magst du mehr darüber erzählen?

Ansonsten: Wenn du eine Tabelle anlegst, meinst du dann mit Hilfe einer Datenbank? Diese kümmert sich in der Regel darum, dass es keine Konflikte gibt. Bei Python 2.5 ist immer sqlite dabei, das könntest du als leichtgewichtige Datenbank nutzen - und sei es nur für das Locking.

Bekanntlich passen relationale Datenbanken und objektorientierte Programmierung nicht wirklich zusammen. Interessant wäre, eine Zope-artige Objektpersistenz auf der Basis von sqlite zu realisieren, indem man jedes Objekt pickelt und dann in einer großen Tabelle unter einer ID ablegt. Ich bin bestimmt nicht der erste mit dieser Idee.

Wenn's die Lock-Datei sein muss, dann würde ich den Code anders kapseln, damit dieses ganze hässliche Geraffel mit den Dateien in einer Funktion verschwindet.

Code: Alles auswählen

def open_with_lock(tablename, func):
    lockfile = "../db/%s.lck" % tablename
    def acquire_lock():
        while True:
            try: return os.open(lockfile, os.O_CREAT+os.O_EXCL) #+os.O_TEMPORARY) # for win
            except OSError: time.sleep(TIME_TO_RETRY) 
    def release_lock(lock):
        os.unlink(lockfile) #only mac/unix, see O_TEMPORARY for win
        os.close(lockfile) 
    lock = acquire_lock()
    try:
        tbl = open("../db/%s.tbl" % tablename)
        try: return func(tbl)
        finally: tbl.close()
    finally:
        release_lock(lock)
        
data = open_with_lock("player", lambda f: r.read())
Alternativ könnte man einen Iterator benutzen. Ich glaube, dass man nur auf Unix-artigen Betriebssystemen eine Datei löschen kann, bevor man sie schließt. Dies ist aber notwendig, damit es hier nicht zu einer "racing condition" kommt. Unter Windows muss man os.O_TEMPORARY benutzen, was aber auf anderen Plattformen nicht existiert.

Stefan
Benutzeravatar
HWK
User
Beiträge: 1295
Registriert: Mittwoch 7. Juni 2006, 20:44

Samstag 26. Juli 2008, 13:43

@Rakanischu: Das könnte aber ein Problem geben, wenn der "lockende" Thread beim Abarbeiten der Anfrage abstürzt und somit das Lockfile nicht mehr löschen kann.
Siehe hierzu auch den verwandten Thread: http://www.python-forum.de/topic-8229.html
MfG
HWK
Rakanischu
User
Beiträge: 3
Registriert: Freitag 25. Juli 2008, 20:28
Kontaktdaten:

Samstag 26. Juli 2008, 16:06

Hmm, stimmt. Dann würde der nachfolgende Thread für immer und ewig - oder eben bis zu seiner Zwangsterminierung seitens der Serversoftware oder eines Absturzes - versuchen, sich die Tabelle zu schnappen und es gäbe nicht einmal sowas wie eine Rückmeldung, dass nach X Anläufen immernoch kein Zugriff erlangt werden konnte.


@sma:
100%ig bin ich mir was die Details angeht auch noch nicht sicher, aber das grundlegende Szenario ist daran angelehnt, dass ich jahrelang Mitglied einer kleinen Gruppe von Foren-RPG'lern war ( und eigentlich immernoch bin, auch wenn die Sache de facto leider derzeit eingeschlafen ist ), die sich mit Science Fiction beschäftigte. Daraus kann ich schonmal eine Menge an Stoff beziehen: Verschiedene Rassen/Nationen mit jeweils eigenen Besonderheiten, Koloniesystem etc.. So eine Mischung aus OGame, Empire Universe und einer Packung eigener Einfälle ( auf die ich stets hoffe ). Das Ganze ist dementsprechend auf einen für Browserspiele sehr kleinen Personenkreis ausgerichtet.
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Samstag 26. Juli 2008, 16:43

HWK hat geschrieben:@Rakanischu: Das könnte aber ein Problem geben, wenn der "lockende" Thread beim Abarbeiten der Anfrage abstürzt und somit das Lockfile nicht mehr löschen kann.
Unter Windows mit O_TEMPORARY kann das eigentlich nicht passieren - immer vorausgesetzt, der Prozess stürzt korrekt ab, bleibt nicht hängen und das Betriebssystem kann hinter ihm aufräumen. Verschiebt man das os.unlink() in einem Beispiel von `release_lock()` direkt hinter das `open()`, ist zumindest unter Linux/Mac das Risiko minimiert.

Stefan
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Samstag 26. Juli 2008, 17:00

Rakanischu hat geschrieben:Daraus kann ich schonmal eine Menge an Stoff beziehen: Verschiedene Rassen/Nationen mit jeweils eigenen Besonderheiten, Koloniesystem etc.. So eine Mischung aus OGame, Empire Universe und einer Packung eigener Einfälle ( auf die ich stets hoffe ). Das Ganze ist dementsprechend auf einen für Browserspiele sehr kleinen Personenkreis ausgerichtet.
OGame und dergleichen kenne ich nur vom Hörensagen. Meines Wissens basieren diese auf der Idee, dass kontinuierlich Spielzeit verstreicht und man daher möglichst oft online sein muss. So etwas hat mich immer abgeschreckt. Vielleicht trauere ich einfach auch nur alten rundenbasierten Postspielen nach... Ich verstehe auch nicht, wieso Spielspaß einzig in Komplexität stecken soll. Manchmal wirkt das so. Was bringen etwa verschiedene Rassen, außer dass man sich mehr Regeln merken muss bzw. jetzt Gegenspieler dadurch schlagen kann, dass man die Regeln besser beherrscht? Diese Kritik bezieht sich aber mehr auf manchmal gemachte Aussagen wie "256 Techologien" oder "1024 Wunderwaffen" derartiger Spiele. Das Brettspiel Twilight Imperium finde ich z.B. recht nett und das ist auch nicht ohne, was die Komplexität angeht (ich habe allerdings nur die 2te und nicht die aktuelle 3te Edition gespielt). Ich erwähne das Spiel, weil es sich IMHO recht gut für Browserspiel für wenige Spieler eignet. Man müsste es jedoch umschreiben, damit die Züge nicht mehr sequentiell sondern parallel durchgeführt werden können und das ändert das Spiel wohl entscheidend (auch gut so, damit man nicht mit Fantasyflightgames aneckt). Wie so viele Dinge, habe ich vor Jahren mal darüber nachgedacht, die Regeln zu implementieren - wie üblich aber nicht in die Tat umgesetzt :(

Oder meinst du ein eher rollspieltechnisches Erzählspiel statt eines Strategiespiels? Auch sowas wird manchmal Forenspiel oder Browserspiel genannt. Dafür braucht man ja eigentlich gar keine (computerimplementierten) Regeln, nur Fantasie (und viel Zeit).

Stefan
Antworten