Multiprozessing und vererbte dict-Klasse

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
Zizibee
User
Beiträge: 229
Registriert: Donnerstag 12. April 2007, 08:36

Hallo zusammen,

ich wollte mein Programm auf mehrere Prozessoren parallel ausführen. Wie ich bereits gelernt habe, brauche ich dafür Multiprozessing, weil Threads in Python das anders umsetzten wie ich es bräuchte. Nun habe ich aber in meinem Programm eine Klasse, die von Dict erbt. Dieses Wörterbuch soll nun von jedem Prozess gefüllt werden, aber leider bekomme es nicht hin, dass sie sich das Wörterbuch teilen.

Ich habe mal versucht ein Minimalbeispiel zu machen, um das Problem besser zu zeigen. Dafür habe ich eine Dict-Klasse so geschrieben, dass bei jedem Zugriff ein Zähler um eins erhöht wird. Ich bräuchte also folgendes Programm, nur mit Multitasking

Code: Alles auswählen

class QDataDict(dict):
    def __init__(self, key="zugriffe"):
        self.anzahl_zugriffe = key
        super().__init__({key: 0})

    def __getitem__(self, key):
        if key != self.anzahl_zugriffe:
            self[self.anzahl_zugriffe] += 1
        return super().__getitem__(key)

    def __setitem__(self, key, value):
        if key != self.anzahl_zugriffe:
            self[self.anzahl_zugriffe] += 1
        super().__setitem__(key, value)


q_data = QDataDict()
q_data["test_1"] = 1
q_data["test_2"] = 2
q_data["test_3"] = 3
q_data["test_4"] = 4
print(f'Test_2: {q_data["test_2"]}')
print(f'Anzahl Zugriffe: {q_data["zugriffe"]}')
print(q_data)
Die Ausgabe sieht dann so aus

Code: Alles auswählen

Test_2: 2
Anzahl Zugriffe: 5
{'zugriffe': 5, 'test_1': 1, 'test_2': 2, 'test_3': 3, 'test_4': 4}
Ohne meine Klasse, also nur mit dem Standard-Dictionary funktioniert es mit dem Multiprozessing:

Code: Alles auswählen

from multiprocessing import Process, Manager

def main(q_data, key, value):
    q_data[key] = value

if __name__ == '__main__':
    with Manager() as manager:
        q_data = manager.dict()

        p1 = Process(target=main, args=(q_data, "test_1", 1,))
        p2 = Process(target=main, args=(q_data, "test_2", 2,))
        p1.start()
        p2.start()
        p1.join()
        p2.join()
    
        print(q_data)
Leider bekomme ich diese beiden Codes nicht zusammen... Ich bekomme das QDataDict einfach nicht mit dem manager.dict() verbunden, so dass der Zähler hochgezählt wird.

Kann mir jemand damit helfen?

Schon einmal vielen Dank!
Sirius3
User
Beiträge: 17741
Registriert: Sonntag 21. Oktober 2012, 17:20

Was soll der Sinn einer Klasse sein, die Item-Zugriffe zählt?
Das macht das Programm doch nur unnötig komplex ohne wirklichen Nutzen.
In einem Multiprocessing-Programm würde man auch nicht das Wörterbuch in jedem Prozess füllen, sondern einfach die ermittelten Werte an das Hauptprogramm zurückgeben und dort ein Wörterbuch erzeugen.
Benutzeravatar
__blackjack__
User
Beiträge: 13080
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Zizibee: Wenn die sich ein Wörterbuch teilen, bringt dann die Parallelisierung überhaupt etwas? Denn die Kommunikation zwischen den Prozessen die dafür notwendig ist, bremst das ganze ja wieder aus, denn die ist verhältnismässig teuer. Gut parallelisieren lassen sich eigentlich eher Algorithmen die Teilergebnisse wirklich unabhängig voneinander berechnen.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Zizibee
User
Beiträge: 229
Registriert: Donnerstag 12. April 2007, 08:36

@ Sirius3: Der Sinn der Klasse besteht darin, als minimales Beispiel mein Problem zu verdeutlichen.
Eigentlich habe ich über Q-Learning einen Computerspieler angelernt, der die Zustände des Spiels und die Gewichtungen der Spielzüge in das Dictionary speichert. Um alles erstmal möglichst einfach zu halten, habe ich mit Tic Tac Toe angefangen. Damit der Spieler schneller lernen kann, wollte ich möglichst wenig Zustände im Dictionary haben und habe daher das Dictionary so angepasst, dass Drehsymmetrien beachtet werden.
Da ich in meinem Rechner 12 Prozessorkern habe, wollte ich nun mehr als einen für das anlernen des Spielers verwenden. Daher die Frage.
Zuletzt geändert von Zizibee am Dienstag 21. März 2023, 22:53, insgesamt 1-mal geändert.
Zizibee
User
Beiträge: 229
Registriert: Donnerstag 12. April 2007, 08:36

@__blackjack__: Ich weiß nicht, ob die Parallelisierung hier etwas bringt oder ob der Kommunikationsaufwand zu groß ist. Daher wollte ich es einfach mal ausprobieren und auf diesem Weg etwas über Threads und Multiprozessing lernen. Mit dem Thema hatte ich mich bisher nämlich noch gar nicht beschäftigt.
Benutzeravatar
__blackjack__
User
Beiträge: 13080
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Zizibee: Ist das denn überhaupt parallelisierbar? Das wäre es ja nur wenn es keinen gemeinsamen Zustand gäbe oder auf den nur *sehr* selten zugegriffen werden muss. Wenn da ständig alle Prozesse auf den selben Daten operieren wird das langsamer, selbst wenn der Mehraufwand kleiner wäre als Interprozesskommunikation, denn selbst dann müsste man den Zugriff auf die gemeinsame(n) Ressource(n) absichern, so das immer nur ein Thread gleichzeitig auf die Daten zugreifen kann, und die anderen dann entsprechend warten.

Falls Du das nicht so umsetzen kannst, das jeder Prozess sein Teilergebnis separat in einem eigenen (abgeleiteten) Wörterbuch berechnen kann, dass dann am Ende zum Hauptprozess zurückgegeben wird und dort zu einem Gesamtergebnis zusammengefasst wird, bringt das im besten Fall kaum etwas, und im schlechtesten Fall wird das sogar deutlich langsamer.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Ernie1412
User
Beiträge: 161
Registriert: Freitag 10. Januar 2020, 20:38

Code: Alles auswählen

from multiprocessing import Process, Manager

class QDataDict(dict):
    def __init__(self, key="zugriffe"):
        self.anzahl_zugriffe = key
        super().__init__({key: 0})

    def __getitem__(self, key):
        if key != self.anzahl_zugriffe:
            self[self.anzahl_zugriffe] += 1
        return super().__getitem__(key)

    def __setitem__(self, key, value):
        if key != self.anzahl_zugriffe:
            self[self.anzahl_zugriffe] += 1
        super().__setitem__(key, value)

class ManagedQDataDict:
    def __init__(self, key="zugriffe"):
        self.manager = Manager()
        self.q_data = self.manager.dict(QDataDict(key))

    def __getitem__(self, key):
        return self.q_data[key]

    def __setitem__(self, key, value):
        self.q_data[key] = value

if __name__ == '__main__':
    q_data = ManagedQDataDict()

    p1 = Process(target=q_data.__setitem__, args=("test_1", 1,))
    p2 = Process(target=q_data.__setitem__, args=("test_2", 2,))
    p1.start()
    p2.start()
    p1.join()
    p2.join()

    print(q_data.q_data)
Benutzeravatar
sparrow
User
Beiträge: 4187
Registriert: Freitag 17. April 2009, 10:28

@Ernie1412: Was möchtest du uns mit dem Code sagen? Dass du deinen Freund chatGPT gefragt hast und "irgendwie Code" dabei heraus gekommen ist? Das Ergebnis ist auf jeden Fall nicht schön und für jedes Objekt eine Manager anzulegen eher nicht Sinn der Sache. Weiß du wofür der Manager da ist?

@Zizibee: Zum Grundlegenden Verständnis, warum das nicht funktioniert, wie du dir das gedacht hast:

Wenn man in die Dokumentation schaut, findet man dort den Hinweis, dass der Aufruf der .dict() Methode eines multiprocessing.Manager-Objektes kein "normales" dict zurück gibt, sondern etwas, das sich danach anfühlt - aber eben etwas mehr ist. Deshalb ist der Gedanke, "einfach so" ein dict im Multiprocessing teilen zu können, nicht ganz richtig.

Was du versuchen könntest: etwas Eigenes mit register dem Manager bekannt zu machen.

Zusätzlich zu der Infragestellung eines dicts als Datenkarussell in diesem Fall: Ich habe bisher gefühlt nie das Bedürfnis gehabt von einem dict zu erben. Das klingt hier ein bisschen so, als würdest du ein dict dazu missbrauchen generische getter- und setter-Methoden zu verwenden. Ich habe also meine Zweifel ob das grundlegend sinnvoll ist.
Zizibee
User
Beiträge: 229
Registriert: Donnerstag 12. April 2007, 08:36

Schon einmal vielen Dank für die bisherigen Antworten, ich hoffe ich komme heute Abend noch etwas zum Ausprobieren

@Ernie1412: Bei dem Code bekomme ich die Fehlermeldung
AttributeError: 'QDataDict' object has no attribute 'anzahl_zugriffe'
Scheinbar wird die __init__ von QDataDict nicht ausgeführt.

@sparrow: Das mit dem register werde ich mir mal anschauen, danke.

Vielleicht nochmal zur Erklärung für mein "Datenkarussell". Je weniger Zustände gespeichert werden müssen, desto schneller kann der Computerspieler lernen. Daher habe ich die Drehsymmetrie eingebaut, denn wenn man das Spielfeld um 90°, 180°, 270° dreht oder zusätzlich noch spiegelt, ändert sich am eigentlichen Spielfeld nichts, es muss aber nicht 8 mal abgespeichert, sondern nur 1 mal.
Also übergebe ich meinem QDataDict das Spielfeld, er sieht es ist noch nicht abgespeichert, dreht es daraufhin um 90° nach links und findet es vielleicht im Dictionary. Also ließt der die dazugehörigen Gewichtungen aus, dreht sie zurück (also um 90° nach rechts) und gibt sie an das Spiel zurück. Da das alles in der QDataDict geschicht, muss man im Spiel darauf keine Rücksicht nehmen und nichts anpassen. Bisher fand ich diese Lösung eigentlich recht schick...
Was würdet ihr vorschlagen, wie man das lösen sollte und warum man das so machen sollte? Abgesehen davon würde es mich interessieren, was gegen meine Lösung spricht (ausser jetzt vielleicht die Probleme mit dem Multiprozessing).
Benutzeravatar
grubenfox
User
Beiträge: 426
Registriert: Freitag 2. Dezember 2022, 15:49

Zum Multiprozessing: spricht etwas dagegen neben den Lern-Prozessen noch einen weiteren Prozess zu starten, der sich nur um das Management des einzigen QDataDict kümmert?

Die Lern-Prozesse würden dann alle mit dem QDataDict-Prozess kommunizieren um die Ergebnisse im QDataDict zu speichern bzw. von dort abzurufen.
Sirius3
User
Beiträge: 17741
Registriert: Sonntag 21. Oktober 2012, 17:20

@grubenfox: der Overhead liegt im Datenaustausch und dem Synchronisieren. Ein weiterer Prozess macht das nur schlimmer. Um übrigen ist der Hauptprozess ja genau dieser "Management"-Prozess.
Benutzeravatar
__blackjack__
User
Beiträge: 13080
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Der `Manager` selbst managed bereits einen Prozess den man explizit starten und stoppen muss `start()`/`shutdown()` beziehungsweise das über ``with`` steuert.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
grubenfox
User
Beiträge: 426
Registriert: Freitag 2. Dezember 2022, 15:49

Bei
Management des einzigen QDataDict
wollte ich ja auch nicht 'Management´ schreiben. Die Betonung bei lag eher bei "Datenbank"-Prozess. Die offenbar nicht funktionierende Idee der geteilten QDataDict-Datenstruktur ausgliedern und durch funktionierende Interprozesskommunikation ersetzen.
Ob nun der Hauptprozess sich nur um die Verwaltung der Unterprozesse (erzeugen, starten, ...) kümmern soll oder nebenher auch noch die Datenverwaltung machen soll ('schreiben in' und 'lesen aus' einem QDatatDict)...
Ich halte das lieber schön getrennt.
Benutzeravatar
sparrow
User
Beiträge: 4187
Registriert: Freitag 17. April 2009, 10:28

@grubenfox: Verstehe ich nicht. Wie kommst du jetzt auf eine Datenbank?

Ich glaube du hast dich da etwas verzettelt.
Der Management-Prozess würde natürlich nicht in die Datenstruktur schreiben oder von dort lesen. Eben dafür sind ja die Prozesse da, die er verwaltet. Das ist sein Job. die anderen Prozesse zu verwalten.

Was willst du denn da noch mehr trennen?
Benutzeravatar
grubenfox
User
Beiträge: 426
Registriert: Freitag 2. Dezember 2022, 15:49

Eben... mehrere Prozesse die das Spiel lernen und dabei schreibend und lesend auf ein QDataDict-Objekt (auch Datenkarussel oder im ersten Posting Wörterbuch genannt) zugreifen wollen.
Der gemeinsame Zugriff funktioniert ja offenbar zwar mit dem 'normalem' Dict der vom Manager zur Verfügung gestellt wird (bzw. dem Proxy der laut Doku eigentlich vom Manager bei ´manager.dict()´ geliefert wird). [siehe erstes Posting]
Aber da ist das QDataDict-Objekt ja noch außen vor....

Daher mein Vorschlag: neben den Prozessen die sich um das lernen des Spiels kümmern, ein weiterer Prozess der sich alleine um ein einziges QDataDict-Objekt kümmert und den anderen Prozessen einen schreibenden/lesenden Zugriff ermöglicht.. (was dann noch ein paar neue zu lösende Probleme mit sich bringen würde)

Aber...
sparrow hat geschrieben: Mittwoch 22. März 2023, 09:33 Was du versuchen könntest: etwas Eigenes mit register dem Manager bekannt zu machen.
"register" scheint laut Doku hier das gesuchte Mittel der Wahl zu sein.
Benutzeravatar
grubenfox
User
Beiträge: 426
Registriert: Freitag 2. Dezember 2022, 15:49

Zizibee hat geschrieben: Mittwoch 22. März 2023, 13:18 Vielleicht nochmal zur Erklärung für mein "Datenkarussell". Je weniger Zustände gespeichert werden müssen, desto schneller kann der Computerspieler lernen. Daher habe ich die Drehsymmetrie eingebaut, denn wenn man das Spielfeld um 90°, 180°, 270° dreht oder zusätzlich noch spiegelt, ändert sich am eigentlichen Spielfeld nichts, es muss aber nicht 8 mal abgespeichert, sondern nur 1 mal.
Also übergebe ich meinem QDataDict das Spielfeld, er sieht es ist noch nicht abgespeichert, dreht es daraufhin um 90° nach links und findet es vielleicht im Dictionary. Also ließt der die dazugehörigen Gewichtungen aus, dreht sie zurück (also um 90° nach rechts) und gibt sie an das Spiel zurück. Da das alles in der QDataDict geschicht, muss man im Spiel darauf keine Rücksicht nehmen und nichts anpassen. Bisher fand ich diese Lösung eigentlich recht schick...
Was würdet ihr vorschlagen, wie man das lösen sollte und warum man das so machen sollte? Abgesehen davon würde es mich interessieren, was gegen meine Lösung spricht (ausser jetzt vielleicht die Probleme mit dem Multiprozessing).
Ob jetzt etwas gegen die Lösung spricht kann ich so nicht beurteilen, da ich mich mit Q-Learning selbst noch nie beschäftigt habe. Wie oft werden denn dabei neue Zustände in das QDataDict gespeichert und wie häufig werden bekannte Zustände im QDataDict gesucht/gefunden?

Wenn ich das obige richtig verstehe, dann kommen bei 7 von 8 Abfragen immer mindestens zwei Drehungen vor (eine (oder mehrere) "Hin"-Drehung(en) vom Spielfeld und die "Rück"-Drehung der Gewichtungen). Dafür läuft das Speichern schnell weil nur ein Zustand anstatt 8 gedrehte/gespiegelte Zustände gespeichert werden. .. und QDataDict bleibt vergleichsweise klein.

Würde man auf der anderen Seite bei Speichern gleich alle 8 gedrehten und gespiegelten Varianten speichern, dann würden dort die Drehungen und Spiegelungen Zeitkosten und QDataDict würde wohl größer werden, anderseits würde beim Abfragen das Hin und Her-Drehen entfallen.

Also ohne genaue Messungen wage ich nicht zu beurteilen was da nun für die eine oder andere Variante spricht...
Vielleicht wenn man beides kombiniert: beim Lernen die Lösung mit hin und rück-Drehungen, später wenn die Daten aus dem QDataDict zum Spielen genutzt werden sollen ein großes QDataDict bei dem die Daten schon in allen gedrehten und gespiegelten Varianten vorliegen?
Zizibee
User
Beiträge: 229
Registriert: Donnerstag 12. April 2007, 08:36

@grubenfox: Das hast du alles richtig verstanden, genau so hatte ich mir das gedacht.
Den Lernerfolg zeitlich zu messen ist nicht so einfach, allerdings hat der Computerspieler mit Drehsymmetrie gefühlt nach 20 min lernen besser gespielt als der ohne Drehsymmetrie nach einer Stunde. Von daher würde ich das gerne in irgendeiner Form beibehalten.

Die Frage was gegen meine Lösung sprich ging in Richtung sparrow, wegen seiner Aussage hier:
sparrow hat geschrieben: Mittwoch 22. März 2023, 09:33 Zusätzlich zu der Infragestellung eines dicts als Datenkarussell in diesem Fall: Ich habe bisher gefühlt nie das Bedürfnis gehabt von einem dict zu erben. Das klingt hier ein bisschen so, als würdest du ein dict dazu missbrauchen generische getter- und setter-Methoden zu verwenden. Ich habe also meine Zweifel ob das grundlegend sinnvoll ist.
Da hätte mich interessiert, wie er das umgesetzt hätte. Das hätte ich vielleicht deutlicher schreiben sollen.
Antworten