Du bist nicht angemeldet.

Stilllegung des Forums
Das Forum wurde am 05.06.2023 nach über 20 Jahren stillgelegt (weitere Informationen und ein kleiner Rückblick).
Registrierungen, Anmeldungen und Postings sind nicht mehr möglich. Öffentliche Inhalte sind weiterhin zugänglich.
Das Team von spieleprogrammierer.de bedankt sich bei der Community für die vielen schönen Jahre.
Wenn du eine deutschsprachige Spieleentwickler-Community suchst, schau doch mal im Discord und auf ZFX vorbei!

Werbeanzeige

Sacaldur

Community-Fossil

  • »Sacaldur« ist der Autor dieses Themas

Beiträge: 2 301

Wohnort: Berlin

Beruf: FIAE

  • Private Nachricht senden

1

30.01.2013, 21:16

[C#, XNA] asynchrone Skriptausführung, Threadsicherheit und Threadsynchronisierung

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
Spieleentwickler in Berlin? (Thema in diesem Forum)
---
Es ist ja keine Schande etwas falsch zu machen, als Programmierer tu ich das täglich, [...].

Nox

Supermoderator

Beiträge: 5 272

Beruf: Student

  • Private Nachricht senden

2

31.01.2013, 01:36

Leider kann ich nur zur Threadinggeschichte wirklich was sagen:
Prinzipiell ist lesen immer threadsicher
Solange nur ein Thread schreibt und viele lesen ist es bei nicht komplexen Datentypen auch in Ordnung (ist halt doof wenn ein Iterator durch zwischendurch einfach ungültig wird-daher nur nicht komplexe Datentypen). Sobald mehr als ein Thread schreibt, muss man auf atomic operations oder critical sections, mutex, shared memory,pipes etc zurückgreifen.
Unter C++ existiert das volatile um sicherzustellen, dass der aktuelle und nicht ein gepufferter Wert genutzt wird.
Wenn du python als Skriptsprache nutzt, könnte dir ggf der GIL in die Quere kommen bei deinem Parallelisierungsbestrebungen. Vorallem wenn du versuchst mehrere Skripte (z.b. UI + AI) parallel auszuführen.
Was würde den gegen ein signals/callback Konzept sprechen um die Pausierung/Permanentabfrage zum umgehen?
PRO Lernkurs "Wie benutze ich eine Doku richtig"!
CONTRA lasst mal die anderen machen!
networklibbenc - Netzwerklibs im Vergleich | syncsys - Netzwerk lib (MMO-ready) | Schleichfahrt Remake | Firegalaxy | Sammelsurium rund um FPGA&Co.

BlueCobold

Community-Fossil

Beiträge: 10 738

Beruf: Teamleiter Mobile Applikationen & Senior Software Engineer

  • Private Nachricht senden

3

31.01.2013, 06:54

Volatile löst aber die Synchronisierung nicht. Volatile stellt nur sicher, dass der Thread den Wert nicht lokal cached und dann eine echte externe Änderung des Werts nicht mitbekommt. Um ein synchronized kommt man dennoch nicht herum, wenn man den Wert beschreiben will. Zum Glück gibt es dieses Keyword in C# ja ;) Solange aber nur einer schreibt und mehrere lesen, reicht natürlich volatile voll aus.
Teamleiter von Rickety Racquet (ehemals das "Foren-Projekt") und von Marble Theory

Willkommen auf SPPRO, auch dir wird man zu Unity oder zur Unreal-Engine raten, ganz bestimmt.[/Sarkasmus]

Sacaldur

Community-Fossil

  • »Sacaldur« ist der Autor dieses Themas

Beiträge: 2 301

Wohnort: Berlin

Beruf: FIAE

  • Private Nachricht senden

4

31.01.2013, 12:27

komischerweise hebt mir Visual Studio das "synchronized" aber nicht farbig hervor
oder arbeitet da mal wieder jemand zu viel mit Java? ;D
bisher hatte ich die Synchronisierung mittels "lock" auf dem zu verändernden Objekt (bspw. der Liste) oder einem separaten Objekt durchgeführt

"volatile" (das Schlüsselwort wiederum gibt es) werde ich wohl brauchen, auch wenn ich bisher keine Probleme durch ein Caching wahrgenommen habe
(und ich werde mir mal davon auch mal die Doku zu Gemüte führen)

Callbacks:
ich müsste irgendwie dafür sorgen, dass die weitere Skriptausführung auch tatsä hlich vom Skriptthread oder zumindest von einem anderen Thread als dem Hauptthread durchgeführt wird
weiterhin soll das Skript dann an der Stelle fortgesetzt werden, an der auch die blockierende Methode aufgerufen wurde

zum Verständnis nochmal etwas genauer beschrieben, was ein Skript ist:
im Geensatz zu bspw. Unity ist ein Skript in meine Fall keine Klasse mit divrsen Methoden oder eie Sammlung von Funktionen, sondern einfach nur eie Abfolge von Befehlen, wie beispielsweise Funktionsaufrufe
hinter den Funktionen, die den Python-Skripten zur Verfügung stehen, verbergen sich in C# implementierte Methoden
das Ssnchronisieren der Threads wird vom C#-Code durchgeführt
zudem wird der Skriptthread aus C#-Code heraus gestartet, welcher dann als erstes anfängt, das Skript auszuführen
wenn im Skript also etwas synchron oder asynchron ausgeführt werden soll, dann sollte sich maximal der Funktionsname oder ein Parameter ändern, nicht aber ein Konstrukt um den Aufruf herum entstehen (nicht im Python-Skript)
Spieleentwickler in Berlin? (Thema in diesem Forum)
---
Es ist ja keine Schande etwas falsch zu machen, als Programmierer tu ich das täglich, [...].

dot

Supermoderator

Beiträge: 9 757

Wohnort: Graz

  • Private Nachricht senden

5

31.01.2013, 12:41

Wieso genau muss das Skript in einem separaten Thread ausgeführt werden? Welchen Vorteil versprichst du dir davon?

Sacaldur

Community-Fossil

  • »Sacaldur« ist der Autor dieses Themas

Beiträge: 2 301

Wohnort: Berlin

Beruf: FIAE

  • Private Nachricht senden

6

31.01.2013, 13:37

Wieso genau muss das Skript in einem separaten Thread ausgeführt werden? Welchen Vorteil versprichst du dir davon?

ich habe nicht geschrieben, dass ich das unbedingt so haben will oder dass ich mir dadurch großartig Vorteile verspreche
allerdings wüsste ich nicht, wie ich es anders umsetzen sollte, ohne dass die Skripte an die "unterbrochene Ausführung" angepasst werden müssen
Spieleentwickler in Berlin? (Thema in diesem Forum)
---
Es ist ja keine Schande etwas falsch zu machen, als Programmierer tu ich das täglich, [...].

David Scherfgen

Administrator

Beiträge: 10 382

Wohnort: Hildesheim

Beruf: Wissenschaftlicher Mitarbeiter

  • Private Nachricht senden

7

31.01.2013, 15:18

C# hat kein "synchronized".
BlueCobold meinte wohl "lock".

BlueCobold

Community-Fossil

Beiträge: 10 738

Beruf: Teamleiter Mobile Applikationen & Senior Software Engineer

  • Private Nachricht senden

8

31.01.2013, 15:34

Ja, ich meinte lock ;) Zu viel Java um die Ohren ;)
Teamleiter von Rickety Racquet (ehemals das "Foren-Projekt") und von Marble Theory

Willkommen auf SPPRO, auch dir wird man zu Unity oder zur Unreal-Engine raten, ganz bestimmt.[/Sarkasmus]

Nox

Supermoderator

Beiträge: 5 272

Beruf: Student

  • Private Nachricht senden

9

31.01.2013, 18:46

Irgendwie werde ich aus der Beschreibung noch nicht so ganz schlau; Geht es dir darum, dass in Python der Programmfluss gesteuert und in C# dann die "Rechenarbeit" durchgeführt wird? In dem Fall rate ich dazu das threading module von Python zu nutzen und entsprechend den GIL auf C# Seite freizugeben, wenn die Berechnung länger dauern.

Gibt es atomic-ops nicht in c#? Man muss ja nicht auf alles gleich mit ner CS,Mutex,Lock schießen.
PRO Lernkurs "Wie benutze ich eine Doku richtig"!
CONTRA lasst mal die anderen machen!
networklibbenc - Netzwerklibs im Vergleich | syncsys - Netzwerk lib (MMO-ready) | Schleichfahrt Remake | Firegalaxy | Sammelsurium rund um FPGA&Co.

Sacaldur

Community-Fossil

  • »Sacaldur« ist der Autor dieses Themas

Beiträge: 2 301

Wohnort: Berlin

Beruf: FIAE

  • Private Nachricht senden

10

31.01.2013, 20:26

Und da ich den Code dafür geschrieben habe und genau weiß, was ich will, habe ich ein paar Probleme damit, zu verstehen, woher Verständnisprobleme kommen könnten... =/

Ich versuche die derzeitige Lage nochmal mit ein wenig Code zu beschreiben.
Hier zunächst erstmal beispielhaft 2 Python-Skripte, einmal für einen Mapwechsel und einmal ein Skript für einen Dialog (der auch fast genau so im "Variablentest" enthalten war):

Quellcode

1
2
3
4
5
disallowMovement()
fadeOut(0.25)
teleportTo("/neues_Mapformat", 1, caller.X - 7, 1)
fadeIn(0.25)
allowMovement()

Quellcode

1
2
3
4
5
6
7
8
9
10
11
12
if var["state"] == 0:
    say("Ich benötige Wasser für mein äußerst wichiges Vorhaben.", called)
    say("Allerdings kann ich mir kein's holen gehen...", called)
    var["state"] = 1
elif var["state"] == 1:
    say("Ich benötige Wasser für mein äußerst wichiges Vorhaben.", called)
elif var["state"] == 2:
    say("Danke für das Wasser!", called)
    say("Nun kann ich mich um mein Vorhaben kümmern.", called)
    var["state"] = 3
else:
    say("Danke für das Wasser!", called)

Die Skripte sollen keine Threads starten und sich auch nicht ändern, nur weil die Handhabung der Parallelisierung sich ändert.
Manche der Funktionen (fadeIn, fadeOut, teleportTo und say in den Skripten) sollen dazu führen, dass das Skript so lange nicht weiter ausgeführt wird, bis der Hauptthread die Abarbeitung erledigt hat (das Bild wurde aus- bzw. eingeblendet) bzw. bis die Bedingung für die Erledigung eingetroffen ist (der Benutzer hat bei einer Nachricht "Ok" oder "Weiter" angeklickt).
Oder mit anderen Worten: sollen diese Funktionen blockierende Funktionen sein.
say ist beispielsweise folgendermaßen (in C#) implementiert:

C#-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void Say(String message, Object caller = null)
{
    if (caller != null)
    {
        ThreadSynchronizer synchronizer;
        if (caller is Mapobject)
        {
            synchronizer = this.gameState.Say(message, (Mapobject)caller);
        }
        else
        {
            synchronizer = this.gameState.Say(message, caller.ToString());
        }
        this.WaitForSync(synchronizer);
    }
}

und WaitForSync:

C#-Quelltext

1
2
3
4
5
6
7
private void WaitForSync(ThreadSynchronizer synchronizer)
{
    while (!synchronizer.Finished)
    {
        Thread.Sleep(1);
    }
}

Der ThreadSynchronizer enthält ausschließlich diese eine Property, die vom Hauptthread ggf. auf true gesetzt wird.
Die Methoden GameState.Say fügen die Nachricht letztendlich einer Liste anzuzeigender Nachrichten hinzu.
Das ist alles, was vom Skriptthread übernommen wird.

Die Skripte sollen nur dann ausgeführt werden, wenn gerade etwas passiert ist (das Ansprechen eines NPC's, das aktivieren eines Triggers o. ä.), dessen Reaktion sie darstellen (der Dialog mit dem NPC oder das Teleportieren des Hauptcharakters auf eine andere Map).
Alles andere (bspw. das Initialisieren des Fensters, das Inputhandling, die Visualisierung oder die Verwaltung der Objekte und Variablen) ist mit C# implementiert (bzw. soll dies mal sein).
Das Skript soll also nicht den Programmfluss steuern.


Ich hoffe mal, dass ich für ein wenig mehr Klarheit sorgen konnte.
Ansonsten kann ich nur auf konkrete Fragen warten, sollten noch Unklarheiten bestehen... =/
Spieleentwickler in Berlin? (Thema in diesem Forum)
---
Es ist ja keine Schande etwas falsch zu machen, als Programmierer tu ich das täglich, [...].

Werbeanzeige