Sprechen im Kontext eines Spiels, das auf openGL-Renderer basiert:
Nehmen wir an, es gibt zwei Themen:
Aktualisiert die Spiellogik und -physik usw. für die Objekte im Spiel
Lässt openGL für jedes Spielobjekt Aufrufe basierend auf Daten in den Spielobjekten zeichnen (dieser Thread 1 wird ständig aktualisiert).
Sofern Sie nicht zwei Kopien jedes Spielobjekts im aktuellen Status des Spiels haben, müssen Sie Thread 1 anhalten, während Thread 2 die Draw-Aufrufe ausführt. Andernfalls werden die Spielobjekte während eines Draw-Aufrufs für dieses Objekt aktualisiert ist unerwünscht!
Das Stoppen von Thread 1, um sicher Draw-Aufrufe von Thread 2 zu tätigen, macht jedoch den gesamten Zweck von Multithreading / Parallelität zunichte
Gibt es dafür einen besseren Ansatz als die Verwendung von Hunderten oder Tausenden oder die Synchronisierung von Objekten / Zäunen, damit die Multicore-Architektur für die Leistung genutzt werden kann?
Ich weiß, dass ich weiterhin Multithreading zum Laden von Texturen und zum Kompilieren von Shadern für Objekte verwenden kann, die noch nicht Teil des aktuellen Spielstatus sind. Wie kann ich dies jedoch für die aktiven / sichtbaren Objekte tun, ohne Konflikte beim Zeichnen und Aktualisieren zu verursachen?
Was ist, wenn ich in jedem Spielobjekt eine separate Synchronisierungssperre verwende? Auf diese Weise blockiert jeder Thread nur ein Objekt (Idealfall) und nicht den gesamten Aktualisierungs- / Zeichnungszyklus! Aber wie teuer ist es, jedes Objekt zu sperren (das Spiel kann tausend Objekte haben)?
quelle
Antworten:
Der von Ihnen beschriebene Ansatz mit Sperren wäre sehr ineffizient und höchstwahrscheinlich langsamer als die Verwendung eines einzelnen Threads. Der andere Ansatz, Kopien von Daten in jedem Thread zu behalten, würde wahrscheinlich "geschwindigkeitsmäßig" gut funktionieren, jedoch mit unerschwinglichen Speicherkosten und Codekomplexität, um die Kopien synchron zu halten.
Hierfür gibt es mehrere alternative Ansätze. Eine beliebte Lösung für das Rendern mit mehreren Threads ist die Verwendung eines doppelten Befehlspuffers. Dies besteht darin, das Renderer-Backend in einem separaten Thread auszuführen, in dem alle Zeichenaufrufe und die Kommunikation mit der Rendering-API ausgeführt werden. Der Front-End-Thread, der die Spielelogik ausführt, kommuniziert mit dem Back-End-Renderer über einen Befehlspuffer (doppelt gepuffert).. Mit diesem Setup haben Sie nach Abschluss eines Frames nur einen Synchronisationspunkt. Während das Front-End einen Puffer mit Renderbefehlen füllt, verbraucht das Back-End den anderen. Wenn beide Fäden gut ausbalanciert sind, sollte keiner verhungern. Dieser Ansatz ist jedoch nicht optimal, da er zu einer Latenz in den gerenderten Frames führt. Außerdem macht der OpenGL-Treiber dies wahrscheinlich bereits in seinem eigenen Prozess, sodass Leistungssteigerungen sorgfältig gemessen werden müssten. Es werden auch bestenfalls nur zwei Kerne verwendet. Dieser Ansatz wurde jedoch in mehreren erfolgreichen Spielen wie Doom 3 und Quake 3 verwendet
Skalierbarere Ansätze, die Multi-Core-CPUs besser nutzen, basieren auf unabhängigen Aufgaben , bei denen Sie eine asynchrone Anforderung auslösen, die in einem sekundären Thread bearbeitet wird, während der Thread, der die Anforderung ausgelöst hat, mit einer anderen Arbeit fortfährt. Die Aufgabe sollte idealerweise keine Abhängigkeiten zu den anderen Threads aufweisen, um Sperren zu vermeiden (vermeiden Sie auch gemeinsame / globale Daten wie die Pest!). Eine aufgabenbasierte Architektur kann in lokalisierten Teilen eines Spiels besser verwendet werden, z. B. beim Berechnen von Animationen, beim Finden von KI-Pfaden, beim Generieren von Prozeduren, beim dynamischen Laden von Szenenrequisiten usw. Spiele sind natürlich ereignisreich, die meisten Arten von Ereignissen sind asynchron Es ist einfach, sie in separaten Threads auszuführen.
Zum Schluss empfehle ich zu lesen:
Threading-Grundlagen für Spiele von Intel.
Effektive Parallelität von Herb Sutter (mehrere Links zu anderen guten Ressourcen auf dieser Seite).
quelle