Wie sollte ich meine Klassen strukturieren, um eine Multithread-Simulation zu ermöglichen?

8

In meinem Spiel gibt es Grundstücke mit Gebäuden (Häuser, Ressourcenzentren). Gebäude wie Häuser haben Mieter, Räume, Add-Ons usw., und es gibt mehrere Werte, die basierend auf all diesen Variablen simuliert werden müssen.

Jetzt möchte ich AndEngine für das Front-End-Material verwenden und einen weiteren Thread erstellen, um die Simulationsberechnungen durchzuführen (möglicherweise auch später AI in diesen Thread aufnehmen). Dies ist so, dass ein ganzer Thread nicht die ganze Arbeit erledigt und Probleme wie das Blockieren verursacht. Dies führt das Problem der Parallelität und Abhängigkeit ein .

Das Währungsproblem ist mein Haupt-UI-Thread, und der Berechnungsthread müsste beide auf alle Simulationsobjekte zugreifen. Ich muss sie threadsicher machen, weiß aber nicht, wie ich die Simulationsobjekte speichern und strukturieren soll, um dies zu ermöglichen.

Das Abhängigkeitsproblem besteht darin, dass meine Berechnungen zur Berechnung von Werten von den Werten anderer Objekte abhängen.

Was wäre der beste Weg, um mein Mieterobjekt im Gebäude mit meinen Berechnungen zu verknüpfen? Hardcode in die Mieterklasse? Was ist ein guter Weg, um Algorithmen zu "speichern", damit sie leicht optimiert werden können?

Ein einfacher fauler Weg wäre, alles in eine Klasse zu packen, die das gesamte Objekt enthält, wie z. B. Grundstücke (die wiederum die Gebäude usw. halten). Diese Klasse würde auch den Spielstatus wie die dem Benutzer zur Verfügung stehende Technologie und Objektpools für Dinge wie Sprites enthalten. Aber das ist ein fauler und gefährlicher Weg, richtig?

Bearbeiten: Ich habe mir Dependency Injection angesehen, aber wie gut kommt das mit einer Klasse zurecht, die andere Objekte enthält? dh mein Grundstück mit einem Gebäude, das einen Mieter und eine Vielzahl anderer Werte hat. DI sieht auch bei AndEngine wie ein Schmerz im Hintern aus.

NiffyShibby
quelle
Nur eine kurze Anmerkung, es gibt keine Bedenken hinsichtlich des gleichzeitigen Zugriffs auf Daten, wenn einer der Zugriffe immer nur schreibgeschützt ist. Solange Sie beim Rendern nur die Rohdaten lesen, um sie zum Rendern zu verwenden, und die Daten während der Verarbeitung nicht aktualisieren, gibt es kein Problem. Ein Thread aktualisiert die Daten, der andere Thread liest sie nur und rendert sie.
James
Nun, der gleichzeitige Zugriff ist immer noch ein Problem, da der Benutzer ein Grundstück kaufen, ein Gebäude auf diesem Grundstück bauen und einen Mieter in ein Haus stecken kann, sodass der Haupt-Thread Daten erstellt und die Daten ändern kann. Der gleichzeitige Zugriff ist weniger ein Problem, sondern vielmehr die Instanz der gemeinsamen Nutzung zwischen dem Hauptthread und dem untergeordneten Thread.
NiffyShibby
Ich spreche von Abhängigkeit als Problem. Leute wie Google Guice denken, dass das Verstecken von Abhängigkeit keine kluge Sache ist. Meine Mieterberechnungen hängen vom Baugrundstück, dem Gebäude und dem Erstellen eines Sprites auf dem Bildschirm ab (ich könnte eine relationale Beziehung zwischen einem Gebäudemieter und dem Erstellen eines Mieter-Sprites an anderer Stelle haben)
NiffyShibby
Ich denke, mein Vorschlag sollte so interpretiert werden, dass die Dinge, die abgefädelt werden, entweder eigenständig sind oder schreibgeschützten Zugriff auf Daten erfordern, die von einem anderen Thread verwaltet werden. Das Rendern wäre ein Beispiel für etwas, das Sie abfädeln könnten Sie benötigen nur Lesezugriff auf die Daten, damit sie angezeigt werden können.
James
1
James, selbst ein schreibgeschützter Zugriff kann eine schlechte Idee sein, wenn ein anderer Thread gerade Änderungen an diesem Objekt vornimmt. Bei einer komplexen Datenstruktur kann es zu einem Absturz kommen, und bei einfachen Datentypen kann es zu einem inkonsistenten Lesen kommen.
Kylotan

Antworten:

4

Ihr Problem ist von Natur aus seriell - Sie müssen eine Aktualisierung der Simulation durchführen, bevor Sie sie rendern können. Das Abladen der Simulation in einen anderen Thread bedeutet einfach, dass der Haupt-UI-Thread nichts tut , während der Simulationsthread tickt (was bedeutet, dass er blockiert ist).

Die landläufigen „best practice“ für die Parallelität ist nicht Ihre Darstellung auf einem Thread und Ihre Simulation auf einem anderen zu setzen, wie Sie vorschlagen. Ich empfehle in der Tat dringend gegen diesen Ansatz. Die beiden Operationen sind natürlich seriell miteinander verbunden, und obwohl sie brutal erzwungen werden können, ist sie nicht optimal und skaliert nicht .

Ein besserer Ansatz besteht darin, Teile des Updates oder Renderings gleichzeitig zu erstellen, das Aktualisieren und Rendern jedoch immer seriell zu belassen. Wenn Sie beispielsweise eine natürliche Grenze in Ihrer Simulation haben (z. B. wenn sich Häuser in Ihrer Simulation nie gegenseitig beeinflussen), können Sie alle Häuser in Eimer mit N Häusern schieben und eine Reihe von Threads aufwickeln, die jeweils einen verarbeiten Bucket, und lassen Sie diese Threads verbinden, bevor der Aktualisierungsschritt abgeschlossen ist. Dies lässt sich viel besser skalieren und eignet sich viel besser für das gleichzeitige Design.

Sie überdenken den Rest des Problems:

Die Abhängigkeitsinjektion ist hier ein roter Hering: Alle Abhängigkeitsinjektion bedeutet wirklich, dass Sie die Abhängigkeiten einer Schnittstelle an Instanzen dieser Schnittstelle übergeben ("injizieren"), normalerweise während der Erstellung.

Das heißt, wenn Sie eine Klasse haben, die a modelliert House , die Dinge über das wissen muss, in dem Citysie sich befindet, dann Housekönnte der Konstruktor folgendermaßen aussehen:

public House( City containingCity ) {
  m_city = containingCity; // Store in a member variable for later access
  ...
}

Nichts Besonderes.

Die Verwendung eines Singletons ist nicht erforderlich (dies geschieht häufig in einigen der wahnsinnig komplexen, überentwickelten "DI-Frameworks" wie Caliburn, die für "Enterprise" -GUI-Anwendungen entwickelt wurden - dies ist keine gute Lösung). Tatsächlich ist die Einführung von Singletons oft das Gegenteil eines guten Abhängigkeitsmanagements. Sie können auch schwerwiegende Probleme mit Multithread-Code verursachen, da sie normalerweise ohne Sperren nicht threadsicher gemacht werden können. Je mehr Sperren Sie erwerben müssen, desto schlimmer war Ihr Problem für die parallele Behandlung geeignet.


quelle
Ich erinnere mich, dass Singletons in meinem ursprünglichen Beitrag schlecht waren ...
NiffyShibby
Ich erinnere mich, dass Singletons in meinem ursprünglichen Beitrag schlecht waren, aber das wurde entfernt. Ich glaube, ich verstehe, was du sagst. Beispiel: Meine kleine Person läuft über einen Bildschirm, während sie tut, dass der Update-Thread aufgerufen wird. Sie muss ihn aktualisieren, kann dies jedoch nicht, da der Haupt-Thread das Objekt verwendet und mein anderer Thread blockiert ist. Wo sollte ich zwischen dem Rendern aktualisieren.
NiffyShibby
Jemand hat mir einen nützlichen Link geschickt. gamedev.stackexchange.com/questions/95/…
NiffyShibby
5

Die übliche Lösung für Parallelitätsprobleme ist die Datenisolation .

Isolation bedeutet, dass jeder Thread seine eigenen Daten hat und keine Daten anderer Threads berührt. Auf diese Weise gibt es keine Probleme mit der Parallelität ... aber dann haben wir ein Kommunikationsproblem. Wie können diese Threads zusammenarbeiten, wenn sie keine Daten gemeinsam nutzen?

Hier gibt es zwei Ansätze.

Erstens ist Unveränderlichkeit . Unveränderliche Strukturen / Variablen sind diejenigen, die ihren Zustand niemals ändern. Das mag zunächst nutzlos klingen - wie kann man eine "Variable" verwenden, die sich nie ändert? Wir können diese Variablen jedoch austauschen ! Betrachten Sie dieses Beispiel: Angenommen, Sie haben eine TenantKlasse mit einer Reihe von Feldern, die sich in einem konsistenten Zustand befinden muss. Wenn Sie ein TenantObjekt in Thread A ändern und es gleichzeitig von Thread B aus beobachten, wird das Objekt in Thread B möglicherweise in einem inkonsistenten Zustand angezeigt. Wenn Tenantes jedoch unveränderlich ist, kann Thread A es nicht ändern. Stattdessen schafft es neue TenantObjekt mit nach Bedarf eingerichteten Feldern und tauscht es gegen das alte aus. Das Austauschen ist nur eine Änderung einer Referenz, die wahrscheinlich atomar ist, und daher gibt es keine Möglichkeit, das Objekt in einem inkonsistenten Zustand zu beobachten.

Der zweite Ansatz ist Versenden von Nachrichten . Die Idee dahinter ist, dass wir diesem Thread sagen können , was mit Daten zu tun ist, wenn alle Daten einem Thread "gehören" . Jeder Thread in dieser Architektur verfügt über eine Nachrichtenwarteschlange - eine Liste von MessageObjekten und eine Messaging-Pumpe - eine ständig ausgeführte Methode, die eine Nachricht aus der Warteschlange entfernt, interpretiert und eine Handlermethode aufruft. Angenommen, Sie haben auf ein Grundstück getippt und signalisiert, dass es gekauft werden muss. Der UI-Thread kann das nicht ändernPlot Objekt nicht direkt , da er zum Logik-Thread gehört (und wahrscheinlich unveränderlich ist). Der UI-Thread erstellt BuyMessagestattdessen ein Objekt und fügt es der Warteschlange des Logik-Threads hinzu. Der Logik-Thread nimmt beim Ausführen die Nachricht aus der Warteschlange und ruft aufBuyPlot()Extrahieren der Parameter aus dem Nachrichtenobjekt. Möglicherweise wird eine Nachricht zurückgesendet, in der beispielsweise der BuySuccessfulMessageUI-Thread angewiesen wird, ein "Jetzt haben Sie mehr Land!" Fenster auf dem Bildschirm. Natürlich muss der Zugriff auf die Nachrichtenwarteschlange mit der Sperre, dem kritischen Abschnitt oder dem in AndEngine genannten Namen synchronisiert werden. Dies ist jedoch ein einzelner Synchronisationspunkt zwischen Threads, und Threads werden für eine sehr kurze Zeit angehalten, sodass dies kein Problem darstellt.

Diese beiden Ansätze werden am besten in Kombination verwendet. Ihre Threads sollten mit Nachrichten kommunizieren und einige unveränderliche Daten für andere Threads "offen" haben - beispielsweise eine unveränderliche Liste von Plots, mit denen die Benutzeroberfläche sie zeichnen kann.

Beachten Sie auch, dass "schreibgeschützt" nicht unbedingt unveränderlich bedeutet ! Jede komplexe Datenstruktur wie eine Hashtabelle kann ihren internen Status bei Lesezugriffen ändern. Überprüfen Sie daher zuerst die Dokumentation.

Keine Ursache
quelle
Das klingt ordentlich, ich muss ein paar Tests damit machen, es klingt ziemlich teuer auf diese Weise, ich habe nach dem Vorbild von DI mit einem Umfang von Singleton gedacht und dann Sperren für den gleichzeitigen Zugriff verwendet. Aber ich habe nie daran gedacht, es so zu machen, es könnte funktionieren: D
NiffyShibby
So machen wir Parallelität auf hochkonkurrierenden Multithread-Servern. Wahrscheinlich ein kleiner Overkill für ein einfaches Spiel, aber das ist der Ansatz, den ich selbst verwenden würde.
Nevermind
4

Wahrscheinlich haben 99% der in der Geschichte geschriebenen Computerprogramme nur einen Thread verwendet und funktionierten einwandfrei. Ich habe keine Erfahrung mit der AndEngine, aber es ist sehr selten, Systeme zu finden, die die Threading erfordern , nur einige, die mit der richtigen Hardware davon hätten profitieren können.

Um Simulation und GUI / Rendering in einem Thread durchzuführen, führen Sie traditionell einfach einen Teil der Simulation durch, rendern dann und wiederholen dies in der Regel viele Male pro Sekunde.

Wenn jemand wenig Erfahrung mit der Verwendung mehrerer Prozesse hat oder nicht genau weiß, was Thread-Sicherheit bedeutet (ein vager Begriff, der viele verschiedene Dinge bedeuten kann), ist es zu einfach, viele Fehler in ein System einzuführen. Daher würde ich persönlich empfehlen, den Single-Threaded-Ansatz zu verwenden, Simulation und Rendering zu verschachteln und alle Threading-Vorgänge für Vorgänge zu speichern, von denen Sie sicher wissen, dass sie lange dauern und unbedingt Threads und kein ereignisbasiertes Modell erfordern.

Kylotan
quelle
Andengine macht das Rendern für mich, aber ich habe immer noch das Gefühl, dass die Berechnungen in einem anderen Thread ausgeführt werden müssen, da der Haupt-UI-Thread langsamer wird, wenn er nicht blockiert wird, wenn alles in einem Thread ausgeführt wird.
NiffyShibby
Warum fühlst du das? Haben Sie Berechnungen, die teurer sind als ein typisches 3D-Spiel? Und wissen Sie, dass die meisten Android-Geräte nur einen Kern haben und daher keinen zusätzlichen Leistungsvorteil durch zusätzliche Threads erzielen?
Kylotan
Nein, aber es ist schön, die Logik zu trennen und klar zu definieren, was getan wird. Wenn Sie sie im selben Thread belassen, müssen Sie auf die Hauptklasse verweisen, in der dies gehalten wird, oder DI mit Singleton-Gültigkeitsbereich ausführen. Welches ist nicht so sehr ein Problem. Was den Kern betrifft, sehen wir, dass mehr Dual-Core-Android-Geräte herauskommen. Meine Spielidee funktioniert auf einem Single-Core-Gerät möglicherweise überhaupt nicht gut, während sie auf einem Dual-Core-Gerät recht gut funktionieren könnte.
NiffyShibby
Das Entwerfen von allem in einem ganzen Thread scheint mir also keine gute Idee zu sein, zumindest mit Threads kann ich die Logik trennen und muss mich in Zukunft nicht mehr darum kümmern, die Leistung zu verbessern, wie ich sie von Anfang an entworfen habe.
NiffyShibby
Ihr Problem ist jedoch immer noch von Natur aus seriell, sodass Sie wahrscheinlich beide Threads blockieren, die darauf warten, dass sie trotzdem verbunden werden, es sei denn, Sie haben die Daten isoliert (dem Render-Thread etwas zu tun, während der Logik-Thread tickt) und einen Frame rendern oder so hinter der Simulation. Der von Ihnen beschriebene Ansatz ist nicht die allgemein anerkannte Best Practice für das Parallelitätsdesign.