Multithreaded Game Loop

7

Ich versuche, eine Multithread-Spielschleife zu implementieren. Ich habe das schon gemacht, musste dafür aber ein paar Schlösser verwenden, was die Leistung ruinierte. Nachdem ich ein bisschen recherchiert hatte, kam mir diese Idee:

Anstatt die Engine-Subsysteme in verschiedene Threads (z. B. Physik, Animation) aufzuteilen, werden alle Subsysteme auf allen Threads ausgeführt. Wenn wir also vier CPUs haben, werden vier Threads erstellt, wobei jeder Thread eine Schleife für alle Subsysteme hat. Die Single-Core-Spieleschleife wird also auf alle vier Threads kopiert. Diese Spielschleifen werden von einer anderen Schleife gesteuert, die Nachrichten (oder "Jobs", "Aufgaben") gemäß Benutzereingaben oder Skripten an einen dieser Threads (abhängig von ihrer Verwendung) sendet. Dies könnte mit einem doppelt gepufferten Befehlspuffer erfolgen.

Für maximale Renderleistung befindet sich nur die Rendering-Schleife in einem Thread. Jetzt denke ich über den besten Weg nach, um mit der Rendering-Schleife zu kommunizieren. Die beste Idee, die ich mir einfallen lassen könnte, ist, wieder einen Befehlspuffer zu verwenden und ihn auszutauschen, wenn die Rendering-Schleife abgeschlossen ist. Auf diese Weise muss die Rendering-Schleife nicht auf eine der Schleifen warten und kann weiter gerendert werden. Wenn die Spielschleife nicht beendet wurde, als die Rendering-Schleife den Puffer ausgetauscht hat, werden alle Befehle danach im nächsten Frame der Rendering-Schleife ausgeführt. Um sicherzustellen, dass alle Objekte gezeichnet werden, auch wenn die Spielschleife noch nicht beendet ist, enthält die Rendering-Schleife alle Objekte, die gezeichnet werden, und zeichnet sie, bis der Befehl zum Beenden des Zeichnens eingeht.

Beispiel

Mein Ziel ist es, die Engine auf CPU-Zahlen skalierbar zu machen und alle Kerne zu verwenden. Ist das ein Weg das zu tun? Was ist der beste Ansatz dafür und wie gehen moderne Motoren damit um?

Liess Jemai
quelle
Ein ähnlicher Ansatz wird auch von Valves Source Engine verwendet. Möglicherweise finden Sie nützliche Informationen zu Google-Fu.
Akaltar

Antworten:

8

Empfohlene Vorgehensweise:

  • Eine zentrale Schleife im Haupt- / Rendering-Thread, die auch Sound, Netzwerkpufferung usw. verwaltet - im Grunde zentralisiert dies die Kommunikation mit dem Betriebssystem und anderen Threads.
  • Alle prozessorintensiven Aufgaben (z. B. Netzaufbau, KI, Physik) können ad-hoc in mundgerechten Arbeitseinheiten an vorhandene Worker-Threads gesendet werden. Diese Threads werden am Leben erhalten und immer wieder für verschiedene Aufgaben verwendet. Ein typisches Implementierungsmuster ist Thread Pooling .

Beispiele hierfür finden Sie in modernen Plattformen wie Unity (WorkerThread), Xamarin (ThreadPool) und HTML5 (WebWorker) sowie in älteren Technologien wie Java 6 (verschiedene Ansätze).

Wenn Sie feststellen, dass Sie viel auf Mutexe warten, müssen Sie wahrscheinlich die Dinge umstrukturieren. Versuchen Sie, Ihre Aufgaben so zu parallelisieren, dass zu einem bestimmten Zeitpunkt nur ein Thread auf einen bestimmten Speicherplatz zugreift. Wenn beispielsweise ein Arbeitsthread kontinuierlich an mundgerechten dynamischen Beleuchtungsaufgaben arbeitet, während ein anderer an KI arbeitet, kann es nicht zu Interessenkonflikten kommen. Alternativ können Sie 2 Arbeitsthreads für AI verwenden, diese jedoch Arbeitseinheiten verwenden, die sich nicht gegenseitig auf die Ergebnisse auswirken.

Und ja, das Rendern sollte unabhängig davon kontinuierlich sein. Kopieren Sie in der Hauptschleife vor dem Rendern die Ergebnisse aller abgeschlossenen Worker-Thread-Aufgaben in Ihr Hauptdatenmodell, wenn sie abgeschlossen sind ( siehe unten ). Versuchen Sie jedoch nicht, dieselben Modelldaten aus dem Renderer zu lesen, die Sie gerade in Arbeitsthreads lesen / schreiben. Das ist offensichtlich ein Rezept für eine Katastrophe.

Ich rate dringend von der Verwendung mehrerer Schleifen ab, die Sie regelmäßig synchronisieren müssen - tatsächlich klingt es nur nach einem Rezept für Schmerzen.

Wissen, wann eine Arbeitseinheit fertig ist und mit der Hauptschleife synchronisieren

Der genaue Mechanismus für die Benachrichtigung, wenn ein Auftrag in einem anderen Thread abgeschlossen wurde, hängt von Ihrer Konfiguration ab: Plattform, Sprache, Threading-Umgebung. Selbst in C ++ gibt es verschiedene Threading-Umgebungen wie Pthreads, OpenMP-Threading usw. Sie werden entweder durch einen Rückruf benachrichtigt oder Sie müssen regelmäßig eine Bedingung aus der Hauptschleife abfragen, um zu überprüfen, ob jede Arbeitseinheit vollständig ist. Suchen Sie nach dem Mechanismus, der in Ihrer Threading-Umgebung verwendet wird. Ich schlage vor, zuerst einige einfache Beispiele zu finden und auszuführen, um die Threading-Konzepte gründlich zu analysieren . Wenn Sie Standard verwenden möchten, gehen Sie zu pthreads, verstehen Sie jedoch, dass es sich um eine Threading-Bibliothek auf sehr niedriger Ebene handelt. PS Wenn Sie pthreads verwenden, lesen Sie das Buch Multi-Threaded Game Engine Design, 1. Ausgabe, behandelt dies ab Seite 84 ... Sie sollten in der Lage sein, die Vorschau in Google Books zu finden.

Ingenieur
quelle
Aber wie synchronisiere ich die Threads mit der Hauptschleife?
Liess Jemai
1
@ LiessJemai implizit mit eingereichten und erledigten Aufgaben
Ratschenfreak
@ LiessJemai Ratschenfreak ist richtig; ausführlichere Erklärung oben hinzugefügt.
Ingenieur
Ist meine Antwort unten eine Möglichkeit, diese Threads zu synchronisieren?
Liess Jemai