Codierung für Multicore auf Xbox 360 und Windows
Seit Jahren hat sich die Leistung von Prozessoren stetig erhöht, und Spiele und andere Programme haben die Vorteile dieser zunehmenden Leistung genutzt, ohne etwas Besonderes tun zu müssen.
Die Regeln wurden geändert. Die Leistung einzelner Prozessorkerne steigt jetzt, wenn überhaupt, sehr langsam. Die Rechenleistung, die auf einem typischen Computer oder in einer typischen Konsole verfügbar ist, wächst jedoch weiter. Der Unterschied besteht darin, dass der größte Teil dieses Leistungsgewinns jetzt durch mehrere Prozessorkerne auf einem einzelnen Computer, häufig auf einem einzelnen Chip, entsteht. Die Xbox 360 CPU verfügt über drei Prozessorkerne auf einem Chip, und ungefähr 70 Prozent der pc-Prozessoren, die im Jahr 2006 verkauft wurden, waren multikernig.
Der Anstieg der verfügbaren Verarbeitungsleistung ist genauso drastisch wie in der Vergangenheit, aber jetzt müssen Entwickler Multithreadcode schreiben, um diese Leistung zu nutzen. Die Multithreadprogrammierung bringt neue Design- und Programmierprobleme mit sich. Dieses Thema enthält einige Tipps für die ersten Schritte mit Multithreadprogrammierung.
Die Bedeutung von gutem Design
Ein guter Multithreadprogrammentwurf ist wichtig, kann aber sehr schwierig sein. Wenn Sie Ihre wichtigsten Spielsysteme zufällig in verschiedene Threads verschieben, werden Sie wahrscheinlich feststellen, dass jeder Thread den größten Teil seiner Zeit damit verbringen wird, auf die anderen Threads zu warten. Diese Art von Entwurf führt zu einer erhöhten Komplexität und einem erheblichen Debugaufwand, praktisch ohne Leistungssteigerung.
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 Multithreadentwurf jeden Synchronisierungs- und Kommunikationspunkt eindeutig dokumentieren und sollte diese Punkte so weit wie möglich minimieren. Wo Threads kommunizieren müssen, erhöht sich der Programmieraufwand, was die Produktivität verringern 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 so einschränken, dass sie nur wenige Male pro Frame kommunizieren, wird die Multithreading-Geschwindigkeit erheblich beschleunigt, ohne dass die Komplexität übermäßig hoch ist.
Typische Aufgaben mit Threading
Einige Arten von Aufgaben haben sich als lösbar erwiesen, in separate Threads gesetzt zu werden. Die folgende Liste soll nicht vollständig sein, sollte aber einige Ideen enthalten.
Darstellung
Rendering – das z. B. das Gehen im Szenendiagramm oder möglicherweise nur das Aufrufen von D3D-Funktionen – macht häufig 50 Prozent oder mehr CPU-Zeit aus. Daher kann das Verschieben des Renderings in einen anderen Thread erhebliche Vorteile haben. Der Updatethread kann eine Art Renderbeschreibungspuffer ausfüllen, den der Renderingthread dann verarbeiten kann.
Der Spielaktualisierungsthread liegt immer einen Frame vor dem Renderthread, was bedeutet, dass er zwei Frames benötigt, bevor Benutzeraktionen auf dem Bildschirm angezeigt werden. Obwohl diese erhöhte Latenz ein Problem darstellen kann, bleibt die Gesamtlatenz aufgrund der erhöhten Framerate durch die Aufteilung der Workload in der Regel akzeptabel.
In den meisten Fällen wird das gesamte Rendering immer noch in einem einzelnen Thread ausgeführt, aber es handelt sich um einen anderen Thread als das Spielupdate.
Das Flag D3DCREATE _ MULTITHREADED wird manchmal verwendet, um das Rendern in einem Thread und die Erstellung von Ressourcen in anderen Threads zu ermöglichen. Dieses Flag wird bei Xbox 360 ignoriert, und Sie sollten die Verwendung auf Windows vermeiden. Bei Windows erzwingt die Angabe dieses Flags, dass D3D viel Zeit für die Synchronisierung aufwendet, wodurch der Renderthread verlangsamt wird.
Dateidekomprimierung
Ladezeiten sind immer zu lang, und das Streamen von Daten in den Arbeitsspeicher ohne Beeinträchtigung der Bildfrequenz kann eine Herausforderung darstellen. Wenn alle Daten aggressiv auf einem Datenträger komprimiert werden, ist die Datenübertragungsgeschwindigkeit von der Festplatte oder optischen Platte weniger wahrscheinlich ein einschränkender Faktor. Bei einem Singlethreadprozessor ist in der Regel nicht genügend Prozessorzeit für die Komprimierung verfügbar, um Ladezeiten zu unterstützen. Auf einem Multiprozessorsystem verwendet die Dateidekomprimierung jedoch CPU-Zyklen, die andernfalls verschwendet würden. Die Ladezeiten und das Streaming werden verbessert. und platzsparend auf dem Datenträger.
Verwenden Sie die Dateidekomprimierung nicht als Ersatz für die Verarbeitung, die während der Produktion erfolgen sollte. Wenn Sie z. B. beim Laden der Ebene einen zusätzlichen Thread für die Analyse von XML-Daten verwenden, verwenden Sie kein Multithreading, um die Benutzerfreundlichkeit des Players zu verbessern.
Wenn Sie einen Dateidekomprimierungsthread verwenden, sollten Sie weiterhin asynchrone Datei-E/A und große Lesefunktionen verwenden, um die Effizienz des Lesens von Daten zu maximieren.
Grafik:Fluff
Es gibt viele grafische Verbesserungen, die das Aussehen des Spiels verbessern, aber nicht unbedingt notwendig sind. Dazu gehören z. B. prozedural generierte Cloudanimationen, Schwingungs- und Beschnittsimulationen, prozedurale Wellen, prozedurales Verfahren, mehr Partikel oder Nicht-Gaming-Physik.
Da sich diese Auswirkungen nicht auf das Spiel auswirken, verursachen sie keine schwierigen Synchronisierungsprobleme– sie können einmal pro Frame oder seltener mit den anderen Threads synchronisiert werden. Darüber hinaus können diese Effekte bei Spielen für Windows einen Mehrwert für Gamer mit CpUs mit mehreren Kernen schaffen, während sie auf Einzelkerncomputern im Hintergrund weggelassen werden, was eine einfache Möglichkeit zur Skalierung über eine Vielzahl von Funktionen bietet.
Physische Effekte
Die Physik kann häufig nicht auf einen separaten Thread gesetzt werden, um parallel zum Spielupdate ausgeführt zu werden, da das Spielupdate in der Regel die Ergebnisse der physikalischen Berechnungen sofort erfordert. Die Alternative zur Multithread-Physik 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 Physikalische Workload so niedrig halten können, dass sie in den Hauptthread passt, ist 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 die Physik ausführt, verwendet es viele Threads, während der Rest der Zeit nur wenige verwendet. Für die Ausführung der Physik auf mehreren Threads muss dies behoben werden, damit die Workload gleichmäßig über den Frame verteilt wird. Wenn Sie eine Multithread-Physikalische Engine schreiben, müssen Sie sorgfältig auf alle Datenstrukturen, Synchronisierungspunkte und den Lastenausgleich achten.
Multithread-Beispielentwürfe
Spiele für Windows müssen auf Computern mit unterschiedlicher Anzahl von CPU-Kernen ausgeführt werden. Die meisten Spielcomputer haben immer noch nur einen Kern, obwohl die Anzahl der Computer mit zwei Kernen schnell zunimmt. Ein typisches Spiel für Windows kann seine Workload in einen Thread für Aktualisierung und Rendering unterteilen, wobei optionale Arbeitsthreads zusätzliche Funktionen hinzufügen. Darüber hinaus werden wahrscheinlich einige Hintergrundthreads für Datei-E/A- und -Netzwerke verwendet. Abbildung 1 zeigt die Threads zusammen mit den Wichtigsten 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 in einen Updatethread, einen Renderingthread und drei Arbeitsthreads aufgeteilt werden kann, wie in Abbildung 2 dargestellt.
Abbildung 2. Threadingentwurf in einem Spiel für Xbox 360

Mit Ausnahme von Datei-E/A und Netzwerkbetrieb haben alle diese Aufgaben das Potenzial, CPU-intensiv genug zu sein, um von ihrem eigenen Hardwarethread zu profitieren. Diese Aufgaben haben auch das Potenzial, unabhängig genug zu sein, dass sie für einen gesamten Frame ohne Kommunikation ausgeführt werden können.
Der Spielupdatethread verwaltet Controllereingaben, KI und Physik und bereitet Anweisungen für die anderen vier Threads vor. Diese Anweisungen werden in Puffern platziert, die sich im Besitz des Spielupdatethreads befinden, sodass keine Synchronisierung erforderlich ist, da die Anweisungen generiert werden.
Am Ende des Frames übergibt der Spielaktualisierungsthread die Anweisungspuffer an die vier anderen Threads und beginnt dann mit der Arbeit am nächsten Frame und füllt einen weiteren Satz von Anweisungspuffern auf.
Da die Update- und Renderingthreads in Lockstep miteinander arbeiten, werden ihre Kommunikationspuffer einfach doppelt gepuffert: Zu einem beliebigen Zeitpunkt füllt der Updatethread einen Puffer, während der Renderthread aus dem anderen liest.
Die anderen Arbeitsthreads sind nicht unbedingt an die Bildfrequenz gebunden. Das Dekomprimieren eines Datenteils kann viel weniger als ein Frame oder viele Frames dauern. Selbst die Simulation von Bekleidung und Härchen 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 der Arbeitsthreads einen Block von Arbeitsanforderungen hinzu. Wenn Sie die Liste nur einmal pro Frame hinzufügen, wird sichergestellt, dass der Aktualisierungsthread den Synchronisierungsaufwand minimiert. Jeder Arbeitsthread pullt Zuweisungen so schnell wie möglich aus der Arbeitswarteschlange und verwendet eine Schleife, die in etwa wie folgt aussieht:
for(;;)
{
while( WorkQueueNotEmpty() )
{
RemoveWorkItemFromWorkQueue();
ProcessWorkItem();
PutResultInDataQueue();
}
WaitForSingleObject( hWorkSemaphore );
}
Da die Daten von den Updatethreads zu den Arbeitsthreads und dann zum Renderthread gelangen, kann es zu einer Verzögerung von drei oder mehr Frames kommen, bevor einige Aktionen auf den Bildschirm gelangen. Wenn Sie den Arbeitsthreads jedoch latenztolerante Aufgaben zuweisen, sollte dies kein Problem darstellen.
Ein alternativer Entwurf wäre, dass mehrere Arbeitsthreads aus derselben Arbeitswarteschlange gezeichnet werden. Dies würde einen automatischen Lastenausgleich ermöglichen und die Wahrscheinlichkeit, dass alle Arbeitsthreads ausgelastet bleiben, wahrscheinlicher machen.
Der Spielupdatethread muss darauf achten, den Arbeitsthreads nicht zu viel Arbeit zuzuweisen, andernfalls können die Arbeitswarteschlangen kontinuierlich zunehmen. Wie der Updatethread dies verwaltet, hängt davon ab, welche Art von Aufgaben die Arbeitsthreads ausführen.
Gleichzeitiges Multithreading und Anzahl von Threads
Alle Threads werden nicht gleich erstellt. Zwei Hardwarethreads können sich auf separaten Chips, auf demselben Chip oder sogar auf demselben Kern befinden. Die wichtigste Konfiguration, die Spielprogrammierer beachten sollten, sind zwei Hardwarethreads auf einem Kern: gleichzeitiges Multithreading (Smt) oder Hyper-Threading Technology (HT Technology).
SMT- oder HT-Technologiethreads nutzen die Ressourcen des CPU-Kerns gemeinsam. Da sie sich die Ausführungseinheiten teilen, beträgt die maximale Beschleunigung durch die Ausführung von zwei Threads anstelle eines Threads in der Regel 10 bis 20 Prozent, anstatt die 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 inkompatibel sind, können sie den Cache um sichten und viele Cachefehler verursachen. Im schlimmsten Fall kann die Gesamtleistung für den CPU-Kern tatsächlich abnehmen, 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 Threadingentwurf ihnen eine zusätzliche Leistung bietet.
Auf Windows ist die Situation komplizierter. Die Anzahl der Threads und ihre Konfiguration variieren von Computer zu Computer, und die Bestimmung der Konfiguration ist kompliziert. Die Funktion GetLogicalProcessorInformation enthält Informationen zur Beziehung zwischen verschiedenen Hardwarethreads. 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 Verweisen.
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 finden Sie an den folgenden Speicherorten:
-
Quelle:
-
DirectX SDK-Stamm \ Beispiele \ für C++ \ Misc \ CoreDetection
-
Ausführbaren:
-
DirectX SDK-Stamm \ Beispiele \ für C++-CoreDetection.exe \ \ \
Die sicherste Annahme besteht darin, nicht mehr als einen CPU-intensiven Thread pro CPU-Kern zu haben. Die Nutzung von cpuintensiveren Threads als CPU-Kerne bietet nur wenige oder gar keine Vorteile und bringt den zusätzlichen Aufwand 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 Methode zum Erstellen eines Threads, zum Warten auf die Beendigung und zum anschließenden 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, bei denen Stapel beim Starten des Threads vollständig ausgeführt werden, kann die Angabe von 0 (null) erheblichen Arbeitsspeicher verschwenden, da viele untergeordnete Threads nicht so viel Stapel benötigen wie das übergeordnete Element. 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) auf 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 die Beendigung des untergeordneten Threads zu warten. Dies ist viel einfacher und effizienter als das Drehen in einer Schleife, die den Threadstatus ü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 WaitForSingleObjectbeendet wird, achten Sie darauf, das Handle erst nach Abschluss der Wartezeit zu schließen.
Auf Xbox 360 müssen Sie Softwarethreads mit XSetThreadProcessor explizit einem bestimmten Hardwarethread zuweisen. Andernfalls verbleiben alle untergeordneten Threads im selben Hardwarethread wie das übergeordnete Thread. Auf Windows können Sie SetThreadAffinityMask verwenden, um dem Betriebssystem dringend vorzuschlagen, auf welchen Hardwarethreads Ihr Thread ausgeführt werden soll. Diese Technik sollte in der Regel auf Windows vermieden werden, da Sie nicht wissen, welche anderen Prozesse möglicherweise auf dem System ausgeführt werden. In der Regel ist es besser, dem Windows Scheduler ihre Threads hardwarethreads im Leerlauf zuweisen zu lassen.
Das Erstellen von Threads ist ein aufwendiger Vorgang. Threads sollten nur selten erstellt und zerstört werden. Wenn Sie threads häufig erstellen und zerstören möchten, verwenden Sie stattdessen einen Pool von Threads, die auf 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 sind mit einem umfangreichen Satz von Synchronisierungsprimitiven ausgestattet. Ausführliche Informationen zu diesen Synchronisierungsprimitiven finden Sie in der Plattformdokumentation.
Exklusiver Zugriff
Der exklusive Zugriff auf eine Ressource, Eine Datenstruktur oder einen Codepfad ist eine häufige Notwendigkeit. 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 jedoch nur für die Synchronisierung innerhalb eines Prozesses und nicht zwischen Prozessen verwendet werden. Ihr Hauptvorteil besteht darin, dass sie ungefähr 20-mal schneller als Mutexe ausgeführt werden.
Ereignisse
Wenn zwei Threads – z. B. ein Updatethread und ein Renderthread – sich 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 extrapoliert einfach zu einer dreifachen Pufferung von Ressourcen.
Semaphoren
Ein Semaphor wird verwendet, um zu steuern, wie viele Threads ausgeführt werden können, und wird häufig verwendet, um Arbeitswarteschlangen zu implementieren. Ein Thread fügt einer Warteschlange Arbeit hinzu und verwendet ReleaseSemaphore, wenn der Warteschlange ein neues Element hinzugefügt wird. Dadurch kann ein Arbeitsthread aus dem Pool der wartenden Threads freigegeben werden. Die Arbeitsthreads rufen einfach WaitForSingleObjectauf, und wenn sie zurückgegeben werden, wissen sie, dass ein Arbeitselement in der Warteschlange für sie vorhanden ist. Darüber hinaus muss ein kritischer Abschnitt oder ein anderes Synchronisierungsverfahren verwendet werden, um den sicheren Zugriff auf die freigegebene Arbeitswarteschlange zu gewährleisten.
Vermeiden von SuspendThread
Manchmal ist es verlockend, suspendThread anstelle der richtigen Synchronisierungsprimitiven zu verwenden, wenn ein Thread seine Arbeit beenden soll. 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 der Bedingungen erfüllt ist. In diesem Fall sollten Sie WaitForMultipleObjectsverwenden.
Interlocked Functions and Lockless Programming
Es gibt eine Reihe von Funktionen zum Ausführen einfacher threadsicherer Vorgänge ohne Verwendung von Sperren. Hierbei handelt es sich um die Interlocked-Funktionsfamilie, z. B. InterlockedIncrement. Diese Funktionen sowie andere Techniken, die das sorgfältige Festlegen von Flags verwenden, werden zusammen als sperrenlose Programmierung bezeichnet. Die sperrlose Programmierung kann äußerst schwierig sein und ist auf Xbox 360 wesentlich schwieriger als auf Windows.
Weitere Informationen zum Programmieren ohne Sperren finden Sie unter Überlegungen zur sperrenlosen Programmierung für Xbox 360 und Microsoft Windows.
Minimieren der Synchronisierung
Einige Synchronisierungsmethoden sind schneller als andere. Anstatt ihren Code jedoch durch Auswahl der schnellstmöglichen Synchronisierungstechniken zu optimieren, ist es in der Regel besser, seltener zu synchronisieren. Dies ist schneller als die zu häufige Synchronisierung und erleichtert das Debuggen von Code.
Einige Vorgänge, z. B. die Speicherbelegung, müssen möglicherweise Synchronisierungsprimitiven verwenden, um ordnungsgemäß zu funktionieren. Daher führt häufige Zuordnungen vom freigegebenen Standardheap zu einer häufigen Synchronisierung, wodurch eine gewisse Leistung verloren geht. Das Vermeiden häufiger Zuordnungen oder die Verwendung von Threadheaps (mit HEAP _ NO _ SERIALIZE bei Verwendung von HeapCreate) kann diese verborgene Synchronisierung vermeiden.
Eine weitere Ursache für die verborgene Synchronisierung ist D3DCREATE _ MULTITHREADED, wodurch D3D auf Windows die Synchronisierung für viele Vorgänge verwendet. (Das Flag wird auf Xbox 360 ignoriert.)
Threadspezifische Daten, auch als lokaler Threadspeicher bezeichnet, können eine wichtige Möglichkeit sein, die Synchronisierung zu vermeiden. Visual C++ können Sie globale Variablen mit der _ _ syntax declspec(thread) als threadspezifisch deklarieren.
__declspec( thread ) int tls_i = 1;
Dadurch erhält jeder Thread im Prozess eine eigene Kopie von 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 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 Hauptthreadfunktion zurückgegeben wird oder der Thread ExitThread oder _ endthreadexaufruft. Wenn ein Thread mit _ beginthreadexerstellt wird, sollte er _ endthreadex verwenden oder von der Hauptthreadfunktion 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 einen Commit committen– sie sollten nie aus dem Hintergrund geschlagen werden.
OpenMP
OpenMP ist eine Spracherweiterung zum Hinzufügen von Multithreading zu Ihrem Programm, indem Pragmas verwendet werden, um den Compiler bei parallelisierenden Schleifen zu leiten. OpenMP wird von Visual C++ 2005 auf Windows und Xbox 360 unterstützt und kann in Verbindung mit der manuellen Threadverwaltung verwendet werden. OpenMP kann eine praktische Möglichkeit zum Multithreaden von Teilen Ihres Codes sein, ist aber wahrscheinlich nicht die ideale Lösung, insbesondere für Spiele. OpenMP ist möglicherweise eher auf länger ausgeführte Produktionsaufgaben anwendbar, z. B. auf die Verarbeitung von Grafik und anderen Ressourcen. Weitere Informationen finden Sie in der Visual C++-Dokumentation oder auf der OpenMP-Website.
Profilerstellung
Multithread-Profilerstellung ist wichtig. Es ist einfach, lange Warteschlangen zu haben, bei denen Threads aufeinander warten. Diese Stags können schwierig zu finden und zu diagnostizieren sein. Erwägen Sie, Ihren Synchronisierungsaufrufen Instrumentierung hinzuzufügen, um sie zu identifizieren. Ein Sampling-Profiler kann auch dabei helfen, diese Probleme zu identifizieren, da er Zeitsteuerungsinformationen aufzeichnen kann, ohne sie erheblich zu ändern.
Zeitliche Steuerung
Die rdtsc-Anweisung ist eine Möglichkeit, genaue Zeitsteuerungsinformationen zu Windows. 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 also zwischen Hardwarethreads wechselt, können große positive oder negative Unterschiede bestehen. Abhängig von den Energieverwaltungseinstellungen kann sich auch die Häufigkeit ändern, mit der die Rdtsc-Indikatorinkremente ausgeführt werden. Um diese Schwierigkeiten zu vermeiden, sollten Sie QueryPerformanceCounter und QueryPerformanceFrequency für die Zeitsteuerung mit hoher Genauigkeit in Ihrem Versandspiel bevorzugen. Weitere Informationen zur zeitlichen Steuerung finden Sie unter Game Timing und Multicore Processors.
Debuggen
Visual Studio multithreaded debugging for Windows and Xbox 360. Im fenster Visual Studio Threads können Sie zwischen Threads wechseln, um die verschiedenen Aufrufstapel und lokalen Variablen anzuzeigen. Im Fenster "Threads" können Sie auch bestimmte Threads einfrieren und beheben.
Auf Xbox 360 können Sie die @ hwthread-Metavariable 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. Visual Studio und andere Microsoft-Debugger ermöglichen es Ihnen, Ihre Threads zu benennen. Implementieren Sie die folgende SetThreadName-Funktion, und rufen Sie sie beim Start 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
Multithreadprogrammierung kann schwierig sein, und einige Multithreadfehler werden nur selten angezeigt, wodurch sie schwer zu finden und zu beheben sind. Eine der besten Möglichkeiten, sie zu leeren, ist das Testen auf einer Vielzahl von Computern, insbesondere auf Computern mit vier oder mehr Prozessoren. Multithreadcode, der auf einem Singlethreadcomputer perfekt funktioniert, kann auf einem Computer mit vier Prozessoren sofort ausfallen. Die Leistungs- und Zeitsteuerungsmerkmale von AMD- und Intel-CPUs können erheblich variieren. Testen Sie daher unbedingt auf Multiprozessorcomputern basierend auf CPUs beider Anbieter.
Windows Verbesserungen an Vista und Windows 7
Für Spiele, die auf neuere 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 Read/Writer-Sperre und die einmalige Initialisierung). Eine Übersicht über diese Technologien finden Sie in den folgenden MSDN Magazine-Artikeln:
- Verbessern der Skalierbarkeit mit neuen Threadpool-APIs
- Neue Synchronisierungsprimitiven Windows Vista
Anwendungen, die Direct3D 11-Features auf diesen Betriebssystemen verwenden, können auch den neuen Entwurf für die gleichzeitige Objekterstellung und verzögerte Kontextbefehlslisten nutzen, um eine bessere Skalierbarkeit für Multithreadrendering zu erzielen.
Zusammenfassung
Mit einem sorgfältigen Entwurf, der die Interaktionen zwischen Threads minimiert, können Sie erhebliche Leistungssteigerungen durch multithreaded-Programmierung erzielen, ohne den Code übermäßig komplex zu gestalten. Dadurch kann Ihr Spielcode die nächste Welle von Prozessorverbesserungen ausführen und immer überzeugendere Spieleerfahrungen bieten.
Referenzen
- Jim Bevefün & Robert Trainer, Multithreading Applications in Win32, Addison-Wesley, 1997
- Chuck Walbourn, Game Timing and Multicore Processors, Microsoft Corporation, 2005
- MSDN Library: GetLogicalProcessorInformation
- OpenMP