Codierung für Multicore auf Xbox 360 und Windows
Seit Jahren ist die Leistung der Prozessoren stetig gestiegen, und Spiele und andere Programme haben die Vorteile dieser wachsenden Leistung ernten, ohne etwas Besonderes tun zu müssen.
Die Regeln wurden geändert. Die Leistung einzelner Prozessorkerne steigt nun sehr langsam, wenn überhaupt. Die in einem typischen Computer oder einer Konsole verfügbare Rechenleistung wächst jedoch weiter. Der Unterschied besteht darin, dass der größte Teil dieses Leistungsgewinns nun aus mehreren Prozessorkernen auf einem einzelnen Computer entsteht, oft in einem einzigen Chip. Die Xbox 360 CPU verfügt über drei Prozessorkerne auf einem Chip, und rund 70 Prozent der 2006 verkauften PC-Prozessoren waren Mehrkerne.
Die Steigerung der verfügbaren Verarbeitungsleistung ist genauso dramatisch wie in der Vergangenheit, aber jetzt müssen Entwickler Multithreadcode schreiben, um diese Leistung nutzen zu können. Multithreadprogrammierung bringt neue Design- und Programmieraufgaben mit sich. In diesem Thema finden Sie Einige Tipps für die ersten Schritte mit der Multithreadprogrammierung.
Die Bedeutung eines guten Designs
Ein guter Multithreadprogrammentwurf ist von entscheidender Bedeutung, kann aber sehr schwierig sein. Wenn Sie Ihre wichtigsten Spielsysteme planlos auf verschiedene Threads verschieben, werden Sie wahrscheinlich feststellen, dass jeder Thread die meiste Zeit damit verbringt, auf die anderen Threads zu warten. Diese Art von Design führt zu einer erhöhten Komplexität und einem erheblichen Debugaufwand, ohne dass die Leistung gesteigert wird.
Jedes Mal, wenn Threads Daten synchronisieren oder freigeben müssen, besteht das Potenzial für Datenbeschädigung, Synchronisierungsaufwand, Deadlocks und Komplexität. Daher muss Ihr Multithreaddesign jeden Synchronisierungs- und Kommunikationspunkt klar dokumentieren und diese Punkte so weit wie möglich minimieren. Wenn Threads kommunizieren müssen, erhöht sich der Programmieraufwand, was die Produktivität senken kann, wenn sich dies auf zu viel Quellcode auswirkt.
Das einfachste Entwurfsziel für Multithreading besteht darin, den Code in große unabhängige Teile aufzuteilen. Wenn Sie diese Teile dann auf die Kommunikation nur ein paar Mal pro Frame beschränken, wird eine erhebliche Beschleunigung durch Multithreading ohne übermäßige Komplexität angezeigt.
Typische Threadaufgaben
Einige Arten von Aufgaben haben sich als in separaten Threads bewährt. Die folgende Liste ist nicht als erschöpfend gedacht, sondern sollte einige Ideen enthalten.
Darstellung
Das Rendern – einschließlich des Durchlaufens des Szenendiagramms oder möglicherweise nur das Aufrufen von D3D-Funktionen – macht häufig 50 Prozent oder mehr der CPU-Zeit aus. Daher kann das Verschieben des Renderings zu einem anderen Thread erhebliche Vorteile haben. Der Updatethread kann eine Art Renderbeschreibungspuffer ausfüllen, den der Renderingthread dann verarbeiten kann.
Der Spielupdatethread ist immer einen Frame vor dem Renderthread, was bedeutet, dass es zwei Frames benötigt, bevor Benutzeraktionen auf dem Bildschirm angezeigt werden. Obwohl diese erhöhte Latenz ein Problem darstellen kann, hält die erhöhte Bildrate durch die Aufteilung der Workload im Allgemeinen die Gesamtlatenz akzeptabel.
In den meisten Fällen wird das gesamte Rendering immer noch in einem einzelnen Thread ausgeführt, aber es ist ein anderer Thread als das Spielupdate.
Das D3DCREATE_MULTITHREADED-Flag wird manchmal verwendet, um das Rendern für einen Thread und die Ressourcenerstellung für andere Threads zu ermöglichen. Dieses Flag wird auf xbox 360 ignoriert, und Sie sollten es unter Windows vermeiden. Unter Windows erzwingt die Angabe dieses Flags, dass D3D eine erhebliche Zeit für die Synchronisierung aufwendet, wodurch der Renderthread verlangsamt wird.
Dekomprimierung von Dateien
Ladezeiten sind immer zu lang, und das Streamen von Daten in den Arbeitsspeicher ohne Auswirkungen auf die Bildfrequenz kann eine Herausforderung sein. Wenn alle Daten aggressiv auf dem Datenträger komprimiert werden, ist die Datenübertragungsgeschwindigkeit von der Festplatte oder optischen Datenträger weniger wahrscheinlich ein einschränkender Faktor. Auf einem Singlethreadprozessor steht normalerweise nicht genügend Prozessorzeit für die Komprimierung zur Verfügung, um Ladezeiten zu erleichtern. Auf einem Multiprozessorsystem verwendet die Dateidekomprimierung jedoch CPU-Zyklen, die andernfalls verschwendet würden. Es verbessert Ladezeiten und Streaming; und spart Speicherplatz auf dem Datenträger.
Verwenden Sie keine Dateidekomprimierung als Ersatz für die Verarbeitung, die während der Produktion erfolgen sollte. Wenn Sie für instance einen zusätzlichen Thread für die Analyse von XML-Daten während des Ebenenladevorgangs verwenden, verwenden Sie kein Multithreading, um die Benutzererfahrung des Spielers zu verbessern.
Wenn Sie einen Dateidekomprimierungsthread verwenden, sollten Sie weiterhin asynchrone Datei-E/A- und große Lesevorgänge verwenden, um die Effizienz des Lesens von Daten zu maximieren.
Grafik fluff
Es gibt viele grafische Schönheiten, die das Aussehen des Spiels verbessern, aber nicht unbedingt notwendig sind. Dazu gehören prozedural generierte Cloudanimationen, Stoff- und Haarsimulationen, prozedurale Wellen, prozedurale Vegetation, weitere Partikel oder Nicht-Gameplay-Physik.
Da sich diese Effekte nicht auf das Gameplay auswirken, verursachen sie keine kniffligen Synchronisierungsprobleme. Sie können sich einmal oder weniger häufig mit den anderen Threads synchronisieren. Darüber hinaus können diese Effekte bei Spielen für Windows einen Mehrwert für Gamer mit Mehrkern-CPUs bieten, während sie auf Einzelkerncomputern im Hintergrund weggelassen werden, sodass eine einfache Möglichkeit zur Skalierung über eine vielzahl von Funktionen ermöglicht wird.
Physische Effekte
Physik kann oft nicht in einen separaten Thread gesetzt werden, der parallel zum Spielupdate ausgeführt werden kann, da das Spielupdate normalerweise die Ergebnisse der Physikberechnungen sofort erfordert. Die Alternative für die Multithreadingphysik besteht darin, sie auf mehreren Prozessoren auszuführen. Obwohl dies möglich ist, ist es eine komplexe Aufgabe, die häufigen Zugriff auf freigegebene Datenstrukturen erfordert. Wenn Sie Ihre Physikworkload so niedrig halten können, dass sie auf den Standard Thread passt, wird Ihr Auftrag einfacher.
Bibliotheken, die die Ausführung von Physik in mehreren Threads unterstützen, sind verfügbar. Dies kann jedoch zu einem Problem führen: Wenn Ihr Spiel Physik ausführt, verwendet es viele Threads, aber in der restlichen Zeit werden nur wenige verwendet. Für das Ausführen von Physik auf mehreren Threads muss dies berücksichtigt werden, damit die Workload gleichmäßig über den Frame verteilt wird. Wenn Sie eine Multithread-Physik-Engine schreiben, müssen Sie alle Datenstrukturen, Synchronisierungspunkte und Lastenausgleich sorgfältig beachten.
Beispiel-Multithreaddesigns
Spiele für Windows müssen auf Computern mit unterschiedlicher Anzahl von CPU-Kernen ausgeführt werden. Die meisten Spielcomputer verfügen immer noch über nur einen Kern, obwohl die Anzahl der Zwei-Kern-Computer schnell wächst. Ein typisches Spiel für Windows kann seine Workload zum Aktualisieren und Rendern in einen Thread aufteilen, mit optionalen Workerthreads zum Hinzufügen zusätzlicher Funktionen. Darüber hinaus würden wahrscheinlich einige Hintergrundthreads für Datei-E/A-Vorgänge und Netzwerke verwendet. Abbildung 1 zeigt die Threads zusammen mit den Standard Datenübertragungspunkten.
Abbildung 1. Threadingdesign in einem Spiel für Windows
Ein typisches Xbox 360-Spiel kann zusätzliche CPU-intensive Softwarethreads verwenden, sodass die Workload wie in Abbildung 2 dargestellt in einen Updatethread, Renderingthread und drei Workerthreads unterteilt werden kann.
Abbildung 2. Threadingdesign in einem Spiel für Xbox 360
Mit Ausnahme der Datei-E/A und des Netzwerks haben diese Aufgaben das Potenzial, cpuintensiv genug zu sein, um von einem eigenen Hardwarethread zu profitieren. Diese Aufgaben haben auch das Potenzial, so unabhängig zu sein, dass sie für einen gesamten Frame ausgeführt werden können, ohne zu kommunizieren.
Der Spielupdatethread verwaltet Controllereingaben, KI und Physik und bereitet Anweisungen für die anderen vier Threads vor. Diese Anweisungen werden in Puffern platziert, die dem Spielupdatethread gehören, sodass keine Synchronisierung erforderlich ist, da die Anweisungen generiert werden.
Am Ende des Frames übergibt der Spielupdatethread die Befehlspuffer an die vier anderen Threads und beginnt dann mit der Arbeit am nächsten Frame, wobei ein weiterer Satz von Anweisungspuffern ausgefüllt wird.
Da die Update- und Renderingthreads im Lockstep miteinander arbeiten, werden ihre Kommunikationspuffer einfach doppelt gepuffert: Zu jedem Zeitpunkt füllt der Updatethread einen Puffer, während der Renderthread aus dem anderen liest.
Die anderen Workerthreads sind nicht unbedingt an die Framerate gebunden. Das Dekomprimieren eines Datenteils kann viel weniger dauern als ein Frame, oder es kann viele Frames dauern. Selbst die Stoff- und Haarsimulation muss möglicherweise nicht genau mit der Bildfrequenz ausgeführt werden, da weniger häufige Updates durchaus akzeptabel sind. Daher benötigen diese drei Threads unterschiedliche Datenstrukturen, um mit dem Updatethread und dem Renderthread zu kommunizieren. Sie benötigen jeweils eine Eingabewarteschlange, die Arbeitsanforderungen enthalten kann, und der Renderthread benötigt eine Datenwarteschlange, die die von den Threads erzeugten Ergebnisse enthalten kann. Am Ende jedes Frames fügt der Updatethread den Warteschlangen von Workerthreads einen Block von Arbeitsanforderungen hinzu. Wenn Sie der Liste nur einmal pro Frame hinzufügen, wird sichergestellt, dass der Aktualisierungsthread den Synchronisierungsaufwand minimiert. Jeder Der Workerthreads ruft Zuweisungen so schnell wie möglich aus der Arbeitswarteschlange ab, wobei eine Schleife verwendet wird, die etwa wie folgt aussieht:
for(;;)
{
while( WorkQueueNotEmpty() )
{
RemoveWorkItemFromWorkQueue();
ProcessWorkItem();
PutResultInDataQueue();
}
WaitForSingleObject( hWorkSemaphore );
}
Da die Daten von den Updatethreads zu den Workerthreads und dann zum Renderthread wechseln, kann es zu einer Verzögerung von drei oder mehr Frames kommen, bevor einige Aktionen auf den Bildschirm gelangen. Wenn Sie den Workerthreads jedoch latenztolerante Aufgaben zuweisen, sollte dies kein Problem darstellen.
Ein alternativer Entwurf wäre, dass mehrere Workerthreads alle aus derselben Arbeitswarteschlange zeichnen. Dies würde einen automatischen Lastenausgleich ermöglichen und es wahrscheinlicher machen, dass alle Workerthreads ausgelastet bleiben.
Der Spielupdatethread muss darauf achten, dass die Workerthreads nicht zu viel Arbeit erhalten, andernfalls können die Arbeitswarteschlangen kontinuierlich wachsen. Wie der Updatethread dies verwaltet, hängt davon ab, welche Art von Aufgaben die Workerthreads ausführen.
Gleichzeitiges Multithreading und Anzahl von Threads
Alle Threads sind nicht gleich erstellt. Zwei Hardwarethreads können sich auf separaten Chips, auf demselben Chip oder sogar auf demselben Kern befinden. Die wichtigste Konfiguration für Spieleprogrammierer sind zwei Hardwarethreads auf einem Kern – Gleichzeitiges Multithreading (SMT) oder Hyper-Threading Technology (HT-Technologie).
SMT- oder HT-Technologiethreads teilen sich die Ressourcen des CPU-Kerns. Da sie die Ausführungseinheiten gemeinsam nutzen, beträgt die maximale Geschwindigkeit beim Ausführen von zwei Threads anstelle eines Threads in der Regel 10 bis 20 Prozent anstelle der 100 Prozent, die von zwei unabhängigen Hardwarethreads möglich sind.
Noch wichtiger ist, dass SMT- oder HT-Technologiethreads die L1-Anweisung und die Datencaches gemeinsam nutzen. Wenn ihre Speicherzugriffsmuster nicht kompatibel sind, können sie sich um den Cache streiten und viele Cachefehler verursachen. Im schlimmsten Fall kann die Gesamtleistung für den CPU-Kern tatsächlich sinken, wenn ein zweiter Thread ausgeführt wird. Auf Xbox 360 ist dies ein recht einfaches Problem. Die Konfiguration der Xbox 360 ist bekannt – drei CPU-Kerne mit jeweils zwei Hardwarethreads – und Entwickler weisen ihre Softwarethreads bestimmten CPU-Threads zu und können messen, ob ihr Threading-Design ihnen zusätzliche Leistung bringt.
Unter Windows ist die Situation komplizierter. Die Anzahl der Threads und deren Konfiguration variiert von Computer zu Computer, und die Bestimmung der Konfiguration ist kompliziert. Die Funktion GetLogicalProcessorInformation gibt Informationen über die Beziehung zwischen verschiedenen Hardwarethreads an, und diese Funktion ist unter Windows Vista, Windows 7 und Windows XP SP3 verfügbar. Daher müssen Sie vorerst die CPUID-Anweisung und die von Intel und AMD angegebenen Algorithmen verwenden, um zu entscheiden, wie viele "echte" Threads Verfügbar sind. Weitere Informationen finden Sie in den Referenzen.
Das CoreDetection-Beispiel im DirectX SDK enthält Beispielcode, der die GetLogicalProcessorInformation-Funktion oder die CPUID-Anweisung verwendet, um die CPU-Kerntopologie zurückzugeben. Die CPUID-Anweisung wird verwendet, wenn GetLogicalProcessorInformation auf der aktuellen Plattform nicht unterstützt wird. CoreDetection befindet sich an den folgenden Speicherorten:
-
Quelle:
-
DirectX SDK root\Samples\C++\Misc\CoreDetection
-
Ausführbaren:
-
DirectX SDK root\Samples\C++\Misc\Bin\CoreDetection.exe
Die sicherste Annahme besteht darin, nicht mehr als einen CPU-intensiven Thread pro CPU-Kern zu haben. Mehr CPU-intensive Threads als CPU-Kerne bietet wenig oder gar keine Vorteile und bringt den zusätzlichen Mehraufwand und die Komplexität zusätzlicher Threads mit sich.
Erstellen von Threads
Das Erstellen von Threads ist ein recht einfacher Vorgang, aber es gibt viele potenzielle Fehler. Der folgende Code zeigt die richtige Möglichkeit, einen Thread zu erstellen, auf dessen Beendigung zu warten und dann zu bereinigen.
const int stackSize = 65536;
HANDLE hThread = (HANDLE)_beginthreadex( 0, stackSize,
ThreadFunction, 0, 0, 0 );
// Do work on main thread here.
// Wait for child thread to complete
WaitForSingleObject( hThread, INFINITE );
CloseHandle( hThread );
...
unsigned __stdcall ThreadFunction( void* data )
{
#if _XBOX_VER >= 200
// On Xbox 360 you must explicitly assign
// software threads to hardware threads.
XSetThreadProcessor( GetCurrentThread(), 2 );
#endif
// Do child thread work here.
return 0;
}
Wenn Sie einen Thread erstellen, haben Sie die Möglichkeit, die Stapelgröße für den untergeordneten Thread anzugeben oder null anzugeben. In diesem Fall erbt der untergeordnete Thread die Stapelgröße des übergeordneten Threads. Auf Xbox 360, wo Stapel beim Starten des Threads vollständig committet werden, kann die Angabe von 0 (null) erheblichen Arbeitsspeicher verschwenden, da viele untergeordnete Threads nicht so viel Stapel benötigen wie die übergeordneten Threads. Auf Xbox 360 ist es auch wichtig, dass die Stapelgröße ein Vielfaches von 64 KB beträgt.
Wenn Sie die CreateThread-Funktion zum Erstellen von Threads verwenden, wird die C/C++-Runtime (CRT) unter Windows nicht ordnungsgemäß initialisiert. Es wird empfohlen, stattdessen die CRT-_beginthreadex-Funktion zu verwenden.
Der Rückgabewert von CreateThread oder _beginthreadex ist ein Threadhandle. Dieser Thread kann verwendet werden, um auf das Beenden des untergeordneten Threads zu warten. Dies ist viel einfacher und viel effizienter als das Drehen in einer Schleife, die den Thread status überprüft. Um auf das Beenden des Threads zu warten, rufen Sie einfach WaitForSingleObject mit dem Threadhandle auf.
Die Ressourcen für den Thread werden erst freigegeben, wenn der Thread beendet und das Threadhandle geschlossen wurde. Daher ist es wichtig, das Threadhandle mit CloseHandle zu schließen, wenn Sie damit fertig sind. Wenn Sie darauf warten, dass der Thread mit WaitForSingleObject beendet wird, sollten Sie das Handle erst schließen, nachdem die Wartezeit abgeschlossen ist.
Auf Xbox 360 müssen Sie einem bestimmten Hardwarethread mithilfe von XSetThreadProcessor explizit Softwarethread-Threads zuweisen. Andernfalls verbleiben alle untergeordneten Threads auf demselben Hardwarethread wie der übergeordnete Thread. Unter Windows können Sie SetThreadAffinityMask verwenden, um dem Betriebssystem nachdrücklich vorzuschlagen, auf welchen Hardwarethreads Ihr Thread ausgeführt werden soll. Diese Technik sollte unter Windows im Allgemeinen vermieden werden, da Sie nicht wissen, welche anderen Prozesse möglicherweise auf dem System ausgeführt werden. Es ist in der Regel besser, den Windows-Planer ihre Threads Hardwarethreads im Leerlauf zuzuweisen.
Das Erstellen von Threads ist ein teurer Vorgang. Threads sollten selten erstellt und zerstört werden. Wenn Sie häufig Threads erstellen und zerstören möchten, verwenden Sie stattdessen einen Pool von Threads, die auf die Arbeit warten.
Synchronisieren von Threads
Damit mehrere Threads zusammenarbeiten können, müssen Sie Threads synchronisieren, Nachrichten übergeben und exklusiven Zugriff auf Ressourcen anfordern können. Windows und Xbox 360 verfügen über einen umfangreichen Satz von Synchronisierungsgrundtypen. Ausführliche Informationen zu diesen Synchronisierungsgrundtypen finden Sie in der Plattformdokumentation.
Exklusiver Zugriff
Es ist häufig erforderlich, exklusiven Zugriff auf eine Ressource, Eine Datenstruktur oder einen Codepfad zu erhalten. Eine Option für den exklusiven Zugriff ist ein Mutex, dessen typische Verwendung hier gezeigt wird.
// Initialize
HANDLE mutex = CreateMutex( 0, FALSE, 0 );
// Use
void ManipulateSharedData()
{
WaitForSingleObject( mutex, INFINITE );
// Manipulate stuff...
ReleaseMutex( mutex );
}
// Destroy
CloseHandle( mutex );
The kernel guarantees that, for a particular mutex, only one thread at a time can
acquire it.
The main disadvantage to mutexes is that they are relatively expensive to acquire
and release. A faster alternative is a critical section.
// Initialize
CRITICAL_SECTION cs;
InitializeCriticalSection( &cs );
// Use
void ManipulateSharedData()
{
EnterCriticalSection( &cs );
// Manipulate stuff...
LeaveCriticalSection( &cs );
}
// Destroy
DeleteCriticalSection( &cs );
Kritische Abschnitte weisen eine ähnliche Semantik wie Mutexe auf, können aber nur verwendet werden, um nur innerhalb eines Prozesses zu synchronisieren, nicht zwischen Prozessen. Ihr Standard Vorteil ist, dass sie etwa zwanzigmal schneller ausgeführt werden als Mutexe.
Ereignisse
Wenn sich zwei Threads – z. B. ein Updatethread und ein Renderthread – mit einem Paar von Renderbeschreibungspuffern abwechseln, benötigen sie eine Möglichkeit, anzugeben, wann sie mit ihrem bestimmten Puffer fertig sind. Dies kann durch Zuordnen eines Ereignisses (zugeordnet mit CreateEvent) zu jedem Puffer erfolgen. Wenn ein Thread mit einem Puffer fertig ist, kann er setEvent verwenden, um dies zu signalisieren, und dann WaitForSingleObject für das Ereignis des anderen Puffers aufrufen. Diese Technik kann problemlos auf die dreifache Pufferung von Ressourcen extrapoliert werden.
Semaphoren
Ein Semaphor wird verwendet, um zu steuern, wie viele Threads ausgeführt werden können, und wird häufig zum Implementieren von Arbeitswarteschlangen verwendet. Ein Thread fügt einer Warteschlange Arbeit hinzu und verwendet ReleaseSemaphore , wenn der Warteschlange ein neues Element hinzugefügt wird. Dadurch kann ein Workerthread aus dem Pool der wartenden Threads freigegeben werden. Die Workerthreads rufen einfach WaitForSingleObject auf, und wenn es zurückgibt, wissen sie, dass sich ein Arbeitselement in der Warteschlange für sie befindet. Darüber hinaus muss ein kritischer Abschnitt oder eine andere Synchronisierungsmethode verwendet werden, um einen sicheren Zugriff auf die freigegebene Arbeitswarteschlange zu gewährleisten.
Vermeiden von SuspendThread
Manchmal ist es verführerisch, suspendThread anstelle der richtigen Synchronisierungsgrundtypen zu verwenden, wenn Sie möchten, dass ein Thread die Ausführung beendet. Dies ist immer eine schlechte Idee und kann leicht zu Deadlocks und anderen Problemen führen. SuspendThread interagiert auch schlecht mit dem Visual Studio-Debugger. Vermeiden Sie SuspendThread. Verwenden Sie stattdessen WaitForSingleObject .
WaitForSingleObject und WaitForMultipleObjects
Die Funktion WaitForSingleObject ist die am häufigsten verwendete Synchronisierungsfunktion. Manchmal möchten Sie jedoch, dass ein Thread wartet, bis mehrere Bedingungen gleichzeitig erfüllt sind oder bis eine von mehreren Bedingungen erfüllt ist. In diesem Fall sollten Sie WaitForMultipleObjects verwenden.
Verzahnte Funktionen und locklose Programmierung
Es gibt eine Reihe von Funktionen zum Ausführen einfacher threadsicherer Vorgänge ohne Sperren. Hierbei handelt es sich um die interlocked-Familie von Funktionen, z. B. InterlockedIncrement. Diese Funktionen und andere Techniken, die das sorgfältige Festlegen von Flags verwenden, werden zusammen als locklose Programmierung bezeichnet. Das programmieren ohne Sperren kann äußerst schwierig sein und ist auf Xbox 360 wesentlich schwieriger als unter Windows.
Weitere Informationen zum Programmieren ohne Sperren finden Sie unter Überlegungen zur locklosen Programmierung für Xbox 360 und Microsoft Windows.
Minimieren der Synchronisierung
Einige Synchronisierungsmethoden sind schneller als andere. Anstatt Ihren Code jedoch zu optimieren, indem Sie die schnellsten Synchronisierungstechniken auswählen, ist es in der Regel besser, seltener zu synchronisieren. Dies ist schneller als eine zu häufige Synchronisierung, und es ermöglicht einfacheren Code, der einfacher zu debuggen ist.
Einige Vorgänge, z. B. die Speicherbelegung, müssen möglicherweise Synchronisierungsgrundtypen verwenden, um ordnungsgemäß zu funktionieren. Daher führen häufige Zuordnungen aus dem freigegebenen Standardheap zu einer häufigen Synchronisierung, was zu einer gewissen Leistungseinbuße führt. Wenn Sie häufige Zuordnungen vermeiden oder Heaps pro Thread verwenden (mit HEAP_NO_SERIALIZE bei Verwendung von HeapCreate), kann diese ausgeblendete Synchronisierung vermieden werden.
Eine weitere Ursache für die ausgeblendete Synchronisierung ist D3DCREATE_MULTITHREADED, wodurch D3D unter Windows die Synchronisierung bei vielen Vorgängen verwendet. (Das Flag wird auf Xbox 360 ignoriert.)
Threadspezifische Daten, auch als lokaler Threadspeicher bezeichnet, können eine wichtige Möglichkeit sein, eine Synchronisierung zu vermeiden. Visual C++ ermöglicht es Ihnen, globale Variablen mit der __declspec(Thread) -Syntax als pro Thread zu deklarieren.
__declspec( thread ) int tls_i = 1;
Dadurch erhält jeder Thread im Prozess eine eigene Kopie der tls_i, auf die sicher und effizient verwiesen werden kann, ohne dass eine Synchronisierung erforderlich ist.
Die __declspec(Thread) -Technik funktioniert nicht mit dynamisch geladenen DLLs. Wenn Sie dynamisch geladene DLLs verwenden, müssen Sie die TLSAlloc-Funktionsfamilie verwenden, um den lokalen Threadspeicher zu implementieren.
Zerstören von Threads
Die einzige sichere Möglichkeit, einen Thread zu zerstören, besteht darin, den Thread selbst zu beenden, indem entweder von der Standard Threadfunktion zurückgegeben wird oder der Thread ExitThread oder _endthreadex aufruft. Wenn ein Thread mit _beginthreadex erstellt wird, sollte er _endthreadex verwenden oder von der Standard-Threadfunktion zurückgeben, da die Verwendung von ExitThread CRT-Ressourcen nicht ordnungsgemäß freigibt. Rufen Sie niemals die TerminateThread-Funktion auf, da der Thread nicht ordnungsgemäß bereinigt wird. Threads sollten immer Selbstmord begehen – sie sollten niemals ermordet werden.
OpenMP
OpenMP ist eine Spracherweiterung zum Hinzufügen von Multithreading zu Ihrem Programm mithilfe von Pragmen, um den Compiler bei der Parallelisierung von Schleifen zu leiten. OpenMP wird von Visual C++ 2005 unter Windows und Xbox 360 unterstützt und kann in Verbindung mit der manuellen Threadverwaltung verwendet werden. OpenMP kann eine bequeme Möglichkeit für Multithread-Teile Ihres Codes sein, ist aber wahrscheinlich nicht die ideale Lösung, insbesondere für Spiele. OpenMP kann für länger laufende Produktionsaufgaben wie die Verarbeitung von Kunst und anderen Ressourcen besser geeignet sein. Weitere Informationen finden Sie in der Visual C++-Dokumentation oder auf der OpenMP-Website.
Profilerstellung
Die Multithreadprofilerstellung ist wichtig. Es ist einfach, lange Stände zu haben, in denen Threads aufeinander warten. Diese Stände können schwierig zu finden und zu diagnostizieren sein. Um sie zu identifizieren, sollten Sie Ihren Synchronisierungsaufrufen Instrumentierung hinzufügen. Ein Sampling-Profiler kann auch dabei helfen, diese Probleme zu identifizieren, da er Zeitinformationen aufzeichnen kann, ohne sie wesentlich zu ändern.
Zeitliche Steuerung
Die rdtsc-Anweisung ist eine Möglichkeit, genaue Zeitinformationen unter Windows zu erhalten. Leider hat rdtsc mehrere Probleme, die es zu einer schlechten Wahl für Ihren Versandtitel machen. Die rdtsc-Leistungsindikatoren werden nicht unbedingt zwischen CPUs synchronisiert. Wenn ihr Thread zwischen Hardwarethreads wechselt, können sie also große positive oder negative Unterschiede erhalten. Abhängig von den Energieverwaltungseinstellungen kann sich auch die Häufigkeit ändern, mit der die Rdtsc-Zählerschritte während des Spiels ausgeführt werden. Um diese Schwierigkeiten zu vermeiden, sollten Sie QueryPerformanceCounter und QueryPerformanceFrequency für hochpräzise Timings in Ihrem Versandspiel bevorzugen. Weitere Informationen zum Timing finden Sie unter Game Timing und Multicore-Prozessoren.
Debuggen
Visual Studio unterstützt das Multithreaddebuggen für Windows und Xbox 360 vollständig. Im Fenster "Visual Studio-Threads" können Sie zwischen Threads wechseln, um die verschiedenen Aufruflisten und lokalen Variablen anzuzeigen. Im Fenster "Threads" können Sie auch bestimmte Threads fixieren und auftauen.
Auf Xbox 360 können Sie die metavariable @hwthread im fenster watch verwenden, um den Hardwarethread anzuzeigen, auf dem der aktuell ausgewählte Softwarethread ausgeführt wird.
Das Threadfenster ist einfacher zu verwenden, wenn Sie Ihre Threads sinnvoll benennen. Mit Visual Studio und anderen Microsoft-Debuggern können Sie Ihre Threads benennen. Implementieren Sie die folgende SetThreadName-Funktion , und rufen Sie sie beim Starten von jedem Thread auf.
typedef struct tagTHREADNAME_INFO
{
DWORD dwType; // must be 0x1000
LPCSTR szName; // pointer to name (in user address space)
DWORD dwThreadID; // thread ID (-1 = caller thread)
DWORD dwFlags; // reserved for future use, must be zero
} THREADNAME_INFO;
void SetThreadName( DWORD dwThreadID, LPCSTR szThreadName )
{
THREADNAME_INFO info;
info.dwType = 0x1000;
info.szName = szThreadName;
info.dwThreadID = dwThreadID;
info.dwFlags = 0;
__try
{
RaiseException( 0x406D1388, 0,
sizeof(info) / sizeof(DWORD),
(DWORD*)&info );
}
__except( EXCEPTION_CONTINUE_EXECUTION ) {
}
}
// Example usage:
SetThreadName(-1, "Main thread");
Der Kerneldebugger (KD) und WinDBG unterstützen auch Multithreaddebuggen.
Testen
Die Multithreadprogrammierung kann schwierig sein, und einige Multithreadfehler werden nur selten angezeigt, sodass sie schwer zu finden und zu beheben sind. Eine der besten Möglichkeiten, sie auszuleeren, ist das Testen auf einer Vielzahl von Computern, insbesondere bei Computern mit vier oder mehr Prozessoren. Multithreadcode, der auf einem Singlethread-Computer perfekt funktioniert, kann auf einem Computer mit vier Prozessoren sofort fehlschlagen. Die Leistungs- und Timingeigenschaften von AMD- und Intel-CPUs können erheblich variieren. Testen Sie daher unbedingt auf Multiprozessorcomputern, die auf CPUs beider Anbieter basieren.
Verbesserungen von Windows Vista und Windows 7
Für Spiele, die auf die neueren Versionen von Windows abzielen, gibt es eine Reihe von APIs, die die Erstellung skalierbarer Multithreadanwendungen vereinfachen können. Dies gilt insbesondere für die neue ThreadPool-API und einige zusätzliche Syncrhonziation-Primitive (Bedingungsvariablen, die schlanke Lese-/Schreibsperre und einmalige Initialisierung). Eine Übersicht über diese Technologien finden Sie in den folgenden MSDN Magazine-Artikeln:
- Verbessern der Skalierbarkeit mit neuen Threadpool-APIs
- Synchronisierungsgrundtypen neu in Windows Vista
Anwendungen, die Direct3D 11-Features auf diesen Betriebssystemen verwenden, können auch den neuen Entwurf für gleichzeitige Objekterstellung und Verzögerte Kontextbefehlslisten nutzen, um eine bessere Skalierbarkeit für multithreaded Rendering zu erzielen.
Zusammenfassung
Mit einem sorgfältigen Entwurf, der die Interaktionen zwischen Threads minimiert, können Sie erhebliche Leistungssteigerungen durch die Multithreadprogrammierung erzielen, ohne Ihren Code zu komplex zu machen. Dies ermöglicht Ihrem Spielcode die nächste Welle von Prozessorverbesserungen und bietet immer überzeugendere Spielerlebnisse.
References
- Jim Beveridge & Robert Weiner, Multithreading-Anwendungen in Win32, Addison-Wesley, 1997
- Chuck Walbourn, Game Timing und Multicore-Prozessoren, Microsoft Corporation, 2005
- MSDN Library: GetLogicalProcessorInformation
- OpenMP