Ich erstelle eine einfache 2D-Spiel-Engine und möchte die Sprites in verschiedenen Threads aktualisieren und rendern, um zu erfahren, wie es gemacht wird.
Ich muss den Update-Thread und den Render-Thread synchronisieren. Derzeit verwende ich zwei Atomflags. Der Workflow sieht ungefähr so aus:
Thread 1 -------------------------- Thread 2
Update obj ------------------------ wait for swap
Create queue ---------------------- render the queue
Wait for render ------------------- notify render done
Swap render queues ---------------- notify swap done
In diesem Setup beschränke ich die FPS des Render-Threads auf die FPS des Update-Threads. Außerdem beschränke ich sleep()
die FPS des Render- und Update-Threads auf 60, damit die beiden Wartefunktionen nicht lange warten.
Das Problem ist:
Die durchschnittliche CPU-Auslastung liegt bei 0,1%. Manchmal sind es bis zu 25% (in einem Quad-Core-PC). Dies bedeutet, dass ein Thread auf den anderen wartet, da die Wartefunktion eine while-Schleife mit einer Test- und Set-Funktion ist und eine while-Schleife alle Ihre CPU-Ressourcen verwendet.
Meine erste Frage lautet: Gibt es eine andere Möglichkeit, die beiden Threads zu synchronisieren? Ich habe festgestellt, dass std::mutex::lock
die CPU nicht verwendet wird, während sie darauf wartet, eine Ressource zu sperren, sodass es sich nicht um eine while-Schleife handelt. Wie funktioniert es? Ich kann nicht verwenden, std::mutex
da ich sie in einem Thread sperren und in einem anderen Thread entsperren muss.
Die andere Frage ist; Da das Programm immer mit 60 FPS ausgeführt wird, warum springt die CPU-Auslastung manchmal auf 25%, was bedeutet, dass eine der beiden Wartezeiten viel wartet? (Die beiden Threads sind beide auf 60 fps begrenzt, sodass sie im Idealfall nicht viel Synchronisation benötigen.)
Edit: Danke für alle Antworten. Zuerst möchte ich sagen, dass ich nicht in jedem Frame einen neuen Thread zum Rendern starte. Ich starte sowohl die Aktualisierungs- als auch die Renderschleife am Anfang. Ich denke, Multithreading kann einige Zeit sparen: Ich habe die folgenden Funktionen: FastAlg () und Alg (). Alg () ist sowohl mein Update-Objekt als auch mein Render-Objekt und Fastalg () ist meine "Render-Warteschlange an" Renderer "senden. In einem einzigen Thread:
Alg() //update
FastAgl()
Alg() //render
In zwei Threads:
Alg() //update while Alg() //render last frame
FastAlg()
Vielleicht kann Multithreading gleichzeitig Zeit sparen. (Tatsächlich in einer einfachen mathematischen Anwendung, in der alg ein langer Algorithmus und fastalg ein schnellerer Algorithmus ist)
Ich weiß, dass Schlaf keine gute Idee ist, obwohl ich nie Probleme habe. Wird das besser?
While(true)
{
If(timer.gettimefromlastcall() >= 1/fps)
Do_update()
}
Dies ist jedoch eine Endlosschleife, die die gesamte CPU belegt. Kann ich den Schlaf (eine Zahl <15) verwenden, um die Nutzung einzuschränken? Auf diese Weise wird es beispielsweise mit 100 fps ausgeführt, und die Aktualisierungsfunktion wird nur 60 Mal pro Sekunde aufgerufen.
Um die beiden Threads zu synchronisieren, verwende ich waitforsingleobject mit createSemaphore, damit ich in verschiedenen Threads sperren und entsperren kann (ohne Verwendung einer while-Schleife), nicht wahr?
quelle
Antworten:
Für eine einfache 2D-Engine mit Sprites ist ein Single-Threaded-Ansatz vollkommen gut. Da Sie jedoch lernen möchten, wie man Multithreading ausführt, sollten Sie lernen, es richtig zu machen.
Unterlassen Sie
sleep
diese Option , um die Bildrate zu steuern. Noch nie. Wenn dir jemand sagt, dass du es tun sollst, schlag ihn.Erstens arbeiten nicht alle Monitore mit 60 Hz. Zweitens werden zwei Timer, die mit der gleichen Geschwindigkeit nebeneinander laufen, immer nicht mehr synchron sein (lassen Sie zwei Pingpong-Bälle aus derselben Höhe auf einen Tisch fallen und hören Sie zu). Drittens
sleep
ist von Natur aus weder genau noch zuverlässig. Die Granularität kann bis zu 15,6 ms betragen (tatsächlich die Standardeinstellung unter Windows [1] ), und ein Frame ist nur 16,6 ms bei 60 fps, sodass für alles andere nur 1 ms übrig bleiben. Außerdem ist es schwierig, 16,6 zu einem Vielfachen von 15,6 zu machen.Außerdem
sleep
darf (und wird es manchmal!) Erst nach 30 oder 50 oder 100 ms oder einer noch längeren Zeit zurückkehren.std::mutex
diese Option, um einen anderen Thread zu benachrichtigen. Dafür ist es nicht da.Tun
sleep
[2] eine viel bessere Genauigkeit und Zuverlässigkeit aufweisen . Außerdem berücksichtigt ein wiederkehrender Timer die Zeit korrekt (einschließlich der dazwischen liegenden Zeit), während das Schlafen für 16,6 ms (oder 16,6 ms minus gemessene_Zeit_verstrichen) dies nicht tut.std::mutex
diese , um jeweils nur einen Thread auf eine Ressource zugreifen zu lassen ("gegenseitig ausschließen") und die seltsame Semantik von einzuhaltenstd::condition_variable
.std::condition_variable
diese Option , um einen anderen Thread zu blockieren, bis eine Bedingung erfüllt ist. Die Semantikstd::condition_variable
dieses zusätzlichen Mutex ist zwar ziemlich seltsam und verdreht (meistens aus historischen Gründen, die von POSIX-Threads geerbt wurden), aber eine Bedingungsvariable ist das richtige Grundelement für das, was Sie wollen.Falls Sie finden
std::condition_variable
zu seltsam mit ihm bequem zu sein, können Sie auch einfach ein Windows - Ereignis verwenden (etwas langsamer) statt oder, wenn Sie mutig sind, bauen Sie Ihre eigenen einfachen Fall um NtKeyedEvents (betrifft die Sachen unheimlich niedrigen Niveau). Wenn Sie DirectX verwenden, sind Sie ohnehin bereits an Windows gebunden, sodass der Verlust der Portabilität kein großes Problem sein sollte.[1] Ja, Sie können die Rate des Schedulers auf 1 ms senken, aber dies ist verpönt, da es viel mehr Kontextwechsel verursacht und viel mehr Strom verbraucht (in einer Welt, in der immer mehr Geräte mobile Geräte sind). Es ist auch keine Lösung, da es den Schlaf immer noch nicht zuverlässiger macht.
[2] Ein Timer erhöht die Priorität des Threads, wodurch er einen anderen Thread mit gleicher Priorität in der Mitte des Quanten unterbrechen und zuerst geplant werden kann, was ein Quasi-RT-Verhalten ist. Es ist natürlich keine echte RT, aber es kommt sehr nahe. Das Aufwachen aus dem Ruhezustand bedeutet lediglich, dass der Thread jederzeit planbar ist, wann immer dies möglich ist.
quelle
Ich bin nicht sicher, was Sie erreichen möchten, indem Sie die FPS von Update und Render auf 60 beschränken. Wenn Sie sie auf denselben Wert beschränken, hätten Sie sie einfach in denselben Thread einfügen können.
Das Ziel beim Trennen von Update und Rendering in verschiedenen Threads besteht darin, beide "fast" unabhängig voneinander zu haben, damit die GPU 500 FPS rendern kann und die Update-Logik weiterhin 60 FPS erreicht. Auf diese Weise erzielen Sie keinen sehr hohen Leistungsgewinn.
Aber Sie sagten, Sie wollten nur wissen, wie es funktioniert, und es ist in Ordnung. In C ++ ist ein Mutex ein spezielles Objekt, mit dem der Zugriff auf bestimmte Ressourcen für andere Threads gesperrt wird. Mit anderen Worten, Sie verwenden einen Mutex, um sinnvolle Daten jeweils nur einem Thread zugänglich zu machen. Dazu ist es ganz einfach:
Quelle: http://en.cppreference.com/w/cpp/thread/mutex
BEARBEITEN : Stellen Sie sicher, dass Ihr Mutex klassen- oder dateiweit ist, wie im angegebenen Link angegeben. Andernfalls erstellt jeder Thread seinen eigenen Mutex und Sie erreichen nichts.
Der erste Thread, der den Mutex sperrt, hat Zugriff auf den darin enthaltenen Code. Wenn ein zweiter Thread versucht, die Funktion lock () aufzurufen, wird er blockiert, bis der erste Thread ihn entsperrt. Ein Mutex ist also im Gegensatz zu einer while-Schleife eine Blockierungsfunktion. Blockierungsfunktionen belasten die CPU nicht.
quelle
std::lock_guard
oder ähnliches, nicht.lock()
/.unlock()
. RAII dient nicht nur der Speicherverwaltung!