Hallo,
meine Frage bezieht sich auf mein derzeitiges Projekt (
[Vorstellung] Python Action Adventure [WiP]). Auch wenn der bisherige Threadtitel vemuten ließe, dass ich Python verwende, stimmt das mittlerweile nicht, da das Spiel fast komplett auf C# umgestellt ist und nur noch ein paar Details fehlen, damit es "wie vorher" ist.
Es gibt zur Ausführungszeit einen Hauptthread, welcher von XNA gesteuert wird und in welchem die Aktualisierung des Spielzustands und das Zeichnen des Zustands vorgenommen werden. Zum Aktualisieren des Zustands gehört es auch, Kollisionen zu erkennen und Skripte (Python) auszuführen, sofern dies notwendig ist.
"Ein Skript ausführen" heißt in diesem Fall, dass ein weiterer Thread gestartet wird, der den Code des Skripts ausführt. Die Skripte können nun diverse Befehle aufrufen, deren Durchführung vom Hauptthread übernommen werden soll.
Mir sind bisher ein paar Ansätze eingefallen, wie die Ausführung der Skripte umgesetzt werden könnte.
Ein Ansatz orientiert sich ein wenig an der Vorgehensweise bei der App-Entwicklung für Windows 8. Dort ist es möglich, eine Async-Methode (per Konvention haben alle Methoden, die mit dem
async-Schlüsselwort versehen sind den Suffix "Async") auszuführen, welche dann grundsätzlich parallel ausgeführt wird. Während dieser Ausführung läuft der eigene Programmcode normal weiter. Möchte man nun das Ergebnis des Methodenaufrufs haben, kann man das
await-Schlüsselwort auf die Rückgabe (ein
Task<T>-Objekt) anwenden. Will man selbst eine Asynchrone Methode definieren, muss man diese mit
async versehen. In eigenen Async-Methoden verläuft die Abarbeitung normal und ohne Parallelisierung, bis das erste Mal auf eine Rückgabe einer anderen Async-Methode mit dem
await-Schlüsselwort gewartet wird. Ab diesem wird dann von der Codestelle, die die eigene Methode aufgerufen hat, die Codeausführung fortgesetzt, bis die Rückgabe in der eigenen Methode verfügbar ist.
Auf mein Spiel übertragen würde das heißen, dass Skripte grundsätzlich nicht parallelisiert ausgeführt werden, sondern dass diese beim Warten auf das Ergebnis einer Funktion pausiert werden, bis das Ergebnis vorhanden ist.
Allerdings bin ich mir bei dieser Variante nicht ganz sicher, in wie weit es mir möglich ist, sie umzusetzen.
Im gegensatz dazu besteht die Möglichkeit, dass entsprechende Funktionen selbst das Warten auf die Abarbeitung einer Aufgabe übernehmen, sollte es notwendig sein. Einfache beispiele dafür wären das Ausblenden des zu sehenden Bildes oder das Anzeigen eines Dialogs, welcher als "Abgearbeitet" gilt, wenn der Spieler diesen bestätigt bzw. "weiterklickt".
Dafür gibt es wiederum verschiedene herangehensweisen.
Unter Python hatte ich in den entsprechenden Funktionen eine Schleife, die prüfte, ob die Aufgabe abgearbeitet wurde (bspw. ob die Nachricht noch angezeigt wird), im Negativfall kurz pausierte und im Positiovfall die Ausführung des Skripts nicht weiter behinderte.
Mein derzeit umgesetzter Ansatz unterscheidet sich nur geringfügig davon. Anstatt auf das entsprechende Objekt zuzugreifen und selbst zu prüfen, ob die Aufgabe erledigt wurde, wird der Funktion ein Objekt übergeben, welches eine Variable enthält, die angibt, ob die Aufgbe erledigt wurde. Der Thread des Skripts prüft weiterhin zyklisch, ob der Wert sich verändert hat und die Abarbeitung somit fertig ist, während der Hauptthread sich um die weitere Ausführung des Spiels kümmert. Wenn dann die Bedingung eintrifft, dass bspw. das Fading fertig dargestellt wurde oder dass der Spieler die Nachricht weggedrückt hat, dann wird der entsprechende Wert gesetzt. Der Skriptthread (oder eher die Schleife der aufgerufenen Funktion) bekommt dies wenige Millisekunden später bzw. in weniger als einer Millisekunde mit und die ausführung des Skripts wird fortgesetzt. Ich halte es aber für recht unelegant, den Thread per
Thread.Sleep zu pausieren, wenn die Bedingung nicht eingetroffen ist und den Thread einfach ohne weiteres in der (fast) Endlosschleife weiterlaufen zu lassen ist keine alternative, da dies die Prozessorauslastung übermäßig hochtreiben würde.
In Java gibt es die Möglichkeit, dass ein Thread auf einem Objekt (in meinem Fall wäre es das Objekt mit der Ergebnisvariable)
wait aufruft, wodurch der Thread pausiert wird, bis auf diesem Objekt
notify aufgerufen wird. Vom Prinzip her ist dies ein etwas besserer Ansatz, als den Thread manuell immer und immer wieder für kurze Zeit schlafen zu legen, allerdings ist mir bisher nichts vergleichbares für C# bekannt und ich habe die Befürchtung, dass so ein
notify untergehen kann, wenn es aufgerufen wird, bevor
wait aufgerufen wird.
Da die Skripte parallelisiert ausgeführt werden, kann es passieren, dass diverse Operationen oder Funktionsaufrufe zu Veränderungen von Werten (vor allem Listen) führen können, auf welche zeitgleich vom Hauptthread zugegriffen wird. Eine Möglichkeit wäre es, einfach so weiter zu machen wie bisher, nur dass der Zugriff auf alle halbwegs kritischen Variablen synchronisiert wird, sodass keine parallelen Zugriffe auf das gleiche Element entstehen können.
Als "Alternative" dazu habe ich mir überlegt, dass grundsätzlich alle Funktionen, die mehr als nur eine Variable ändern würden, dem Hauptthread mitteilen, dass er eine bestimmte Aufgabe zu erledigen hat und dieser die notwendigen Schritte dafür durchführt. Das hat zur Folge, dass die dem Skript zur Verfügung stehenden Funktionen auf weniger Ressourcen zugreifen, die synchronisiert werden müssen und dass es einfacher ist, eine Funktion (aus Sicht des Skriptthreads) asynchron auszuführen. Weiterhin wird dadurch auch das "Pausieren" von Threads relativ einfach. Solange der Hauptthread die Abarbeitung einer Anweisung nicht fortsetzt, gibt er nicht das Signal, dass die Abarbeitung fertig ist und so lange wird der Skriptthread auf die Fertigstellung warten.
Es dürfte wohl eher zu den Grundlagen der Parallelisierung gehören, aber bei welchen Operationen muss man Threads synchronisieren? Ich würde darauf tippen, dass dies grundsätzlich bei allen Operationen gemacht werden muss, die den Zustand eines Objekts verändern (bspw. das Hinzufügen von Elementen in eine Liste) und nicht bei einfachen Zuweisungen von Werten oder Objekten an Variablen. Ich muss zugeben, dass diese Beschreibung eigentlich ein widerspruch in sich ist, da der Zustand eines Objekts durch die Werte seiner Variablen bestimmt wird. Eine Richtigstellung wäre an dieser Stelle sehr nützlich. Bisher habe ich auch für das weiter oben angedeutete Objekt, welches lediglich einen Boolean beinhaltet, eine Threadsynchronisierung beim Zugriff auf diesen durchgeführt, einfach aus der Ungewissheit heraus.
In wie weit sind meine bisherigen Ansätze brauchbar bzw. sinnvoll? Gibt es weiterhin noch Dinge, die ich beachten müsste?
Und anbei zu meinem Projekt: es ist nun schon wieder eine lange Zeit her, dass ich in dem Thema nichts geschrieben habe. Ich werde vlt. die nächsten Tage wieder etwas schreiben.
Sacaldur