Condividi tramite


Codifica per multicore in Xbox 360 e Windows

Per anni le prestazioni dei processori sono aumentate costantemente, e i giochi e altri programmi hanno raccolto i vantaggi di questa potenza crescente senza dover fare nulla di speciale.

Le regole sono state modificate. Le prestazioni dei core a processore singolo stanno aumentando molto lentamente, se affatto. Tuttavia, la potenza di calcolo disponibile in un computer o in una console tipica continua a crescere. La differenza è che la maggior parte di questo miglioramento delle prestazioni deriva ora dalla presenza di più core del processore in un singolo computer, spesso in un singolo chip. La CPU Xbox 360 ha tre core di processore su un chip e circa il 70% dei processori PC venduti nel 2006 erano multi-core.

Gli aumenti della potenza di elaborazione disponibile sono altrettanto drammatici come in passato, ma ora gli sviluppatori devono scrivere codice multithreading per usare questa potenza. La programmazione multithread comporta nuove sfide di progettazione e programmazione. Questo argomento fornisce alcuni consigli su come iniziare a usare la programmazione multithreading.

L'importanza del buon design

Una buona progettazione di programmi multithreading è fondamentale, ma può essere molto difficile. Se sposta in modo casuale i sistemi di gioco principali in thread diversi, è probabile che ogni thread spenda la maggior parte del tempo in attesa sugli altri thread. Questo tipo di progettazione comporta un aumento della complessità e un notevole sforzo di debug, senza praticamente ottenere alcun miglioramento delle prestazioni.

Ogni volta che i thread devono sincronizzare o condividere dati, il potenziale di danneggiamento dei dati, l'overhead di sincronizzazione, i deadlock e la complessità. Pertanto, la progettazione multithreading deve documentare chiaramente ogni punto di sincronizzazione e comunicazione e deve ridurre al minimo tali punti il più possibile. Quando i thread devono comunicare, il lavoro di scrittura del codice aumenterà, che può ridurre la produttività se influisce su un numero eccessivo di codice sorgente.

L'obiettivo di progettazione più semplice per il multithreading è suddividere il codice in parti indipendenti di grandi dimensioni. Se poi si limitano questi pezzi a comunicare solo poche volte per fotogramma, si noterà una velocità significativa dal multithreading, senza eccessiva complessità.

Attività threading tipiche

Alcuni tipi di attività hanno dimostrato di poter essere inseriti in thread separati. L'elenco seguente non è destinato a essere esaustivo, ma dovrebbe dare alcune idee.

Rendering

Rendering, che può includere l'uso del grafico della scena o, possibilmente, solo delle funzioni D3D, spesso rappresenta il 50% o più tempo della CPU. Pertanto, lo spostamento del rendering in un altro thread può avere vantaggi significativi. Il thread di aggiornamento può compilare un qualche tipo di buffer di descrizione di rendering, che il thread di rendering può quindi elaborare.

Il thread di aggiornamento del gioco è sempre un fotogramma davanti al thread di rendering, il che significa che richiede due fotogrammi prima che le azioni dell'utente vengano visualizzate sullo schermo. Anche se questa maggiore latenza può essere un problema, l'aumento della frequenza dei fotogrammi dalla suddivisione del carico di lavoro in genere mantiene accettabile la latenza totale.

Nella maggior parte dei casi il rendering viene ancora eseguito su un singolo thread, ma è un thread diverso dall'aggiornamento del gioco.

Il flag D3DCREATE_MULTITHREADED viene talvolta usato per consentire il rendering su un thread e la creazione di risorse in altri thread; questo flag viene ignorato in Xbox 360 ed è consigliabile evitare di usarlo in Windows. In Windows specificare questo flag forza D3D a dedicare una quantità significativa di tempo alla sincronizzazione, rallentando così il thread di rendering.

Decompressione file

I tempi di caricamento sono sempre troppo lunghi e lo streaming dei dati in memoria senza influire sulla frequenza dei fotogrammi può risultare difficile. Se tutti i dati vengono compressi in modo aggressivo sul disco, la velocità di trasferimento dei dati dal disco rigido o dal disco ottico è meno probabile che sia un fattore di limitazione. In un processore a thread singolo, in genere non è disponibile un tempo di processore sufficiente per la compressione per facilitare i tempi di caricamento. In un sistema multiprocessore, tuttavia, la decompressione dei file usa cicli di CPU che altrimenti verrebbero sprecate; migliora i tempi di caricamento e lo streaming; e risparmia spazio sul disco.

Non usare la decompressione dei file come sostituzione per l'elaborazione che deve essere eseguita durante l'ambiente di produzione. Ad esempio, se si dedica un thread aggiuntivo all'analisi dei dati XML durante il caricamento a livello, non si usa il multithreading per migliorare l'esperienza del giocatore.

Quando si usa un thread di decompressione di file, è comunque consigliabile usare operazioni di I/O asincrone e letture di grandi dimensioni per ottimizzare l'efficienza di lettura dei dati.

S fluff grafico

Ci sono molte bellezze grafiche che migliorano l'aspetto del gioco, ma non sono strettamente necessari. Questi includono elementi come animazioni cloud generate proceduralmente, simulazioni di panno e capelli, onde procedurali, vegetazione procedurale, più particelle o fisica non di gioco.

Poiché questi effetti non influiscono sul gioco, non causano problemi di sincronizzazione complicati, ma possono essere sincronizzati con gli altri thread una volta per fotogramma o meno spesso. Inoltre, nei giochi per Windows questi effetti possono aggiungere valore ai giocatori con CPU multicore, mentre vengono omessi in modo silenzioso su computer a core singolo, offrendo così un modo semplice di ridimensionamento in un'ampia gamma di funzionalità.

Fisica

La fisica spesso non può essere messa in un thread separato da eseguire in parallelo con l'aggiornamento del gioco perché l'aggiornamento del gioco richiede in genere i risultati dei calcoli fisici immediatamente. L'alternativa alla fisica del multithreading è eseguirla su più processori. Anche se questa operazione può essere eseguita, è un'attività complessa che richiede l'accesso frequente alle strutture di dati condivise. Se è possibile mantenere il carico di lavoro fisico abbastanza basso da adattarsi al thread principale, il processo sarà più semplice.

Sono disponibili librerie che supportano l'esecuzione della fisica su più thread. Tuttavia, questo può causare un problema: quando il gioco è in esecuzione fisica, usa molti thread, ma il resto del tempo usa pochi. L'esecuzione della fisica su più thread richiederà di risolvere questo problema in modo che il carico di lavoro venga distribuito uniformemente nel frame. Se si scrive un motore di fisica multithreading, è necessario prestare attenzione a tutte le strutture di dati, i punti di sincronizzazione e il bilanciamento del carico.

Esempi di progetti multithreading

I giochi per Windows devono essere eseguiti in computer con diversi numeri di core CPU. La maggior parte dei computer di gioco ha ancora un solo core, anche se il numero di macchine a due core è in rapida crescita. Un gioco tipico per Windows potrebbe suddividere il carico di lavoro in un thread per l'aggiornamento e il rendering, con thread di lavoro facoltativi per l'aggiunta di funzionalità aggiuntive. Inoltre, è probabile che vengano usati alcuni thread in background per l'I/O dei file e la rete. La figura 1 mostra i thread, insieme ai punti di trasferimento dati principali.

Figura 1. Progettazione del threading in un gioco per Windows

threading design in a game for windows

Un tipico gioco Xbox 360 può usare thread software a elevato utilizzo di CPU, quindi potrebbe suddividere il carico di lavoro in un thread di aggiornamento, un thread di rendering e tre thread di lavoro, come illustrato nella figura 2.

Figura 2. Progettazione del threading in un gioco per Xbox 360

threading design in a game for xbox 360

Ad eccezione dell'I/O e della rete dei file, queste attività hanno tutte il potenziale di un utilizzo sufficientemente elevato della CPU per trarre vantaggio dal proprio thread hardware. Queste attività hanno anche il potenziale di essere sufficientemente indipendenti da poter essere eseguite per un intero frame senza comunicare.

Il thread di aggiornamento del gioco gestisce l'input del controller, l'intelligenza artificiale e la fisica e prepara le istruzioni per gli altri quattro thread. Queste istruzioni vengono inserite nei buffer di proprietà del thread di aggiornamento del gioco, quindi non è necessaria alcuna sincronizzazione perché vengono generate le istruzioni.

Alla fine del fotogramma, il thread di aggiornamento del gioco distribuisce i buffer di istruzioni ai quattro altri thread e quindi inizia a lavorare sul frame successivo, compilando un altro set di buffer di istruzioni.

Poiché i thread di aggiornamento e rendering funzionano tra loro, i buffer di comunicazione vengono semplicemente doppio buffer: in qualsiasi momento, il thread di aggiornamento riempie un buffer mentre il thread di rendering legge dall'altro.

Gli altri thread di lavoro non sono necessariamente associati alla frequenza dei fotogrammi. La decompressione di una parte di dati può richiedere molto meno di un fotogramma o può richiedere molti fotogrammi. Anche la simulazione del panno e dei capelli potrebbe non dover essere eseguita esattamente alla frequenza dei fotogrammi perché gli aggiornamenti meno frequenti possono essere abbastanza accettabili. Di conseguenza, questi tre thread necessitano di strutture di dati diverse per comunicare con il thread di aggiornamento e il thread di rendering. Ognuno di essi necessita di una coda di input in grado di contenere richieste di lavoro e il thread di rendering necessita di una coda di dati in grado di contenere i risultati generati dai thread. Alla fine di ogni frame, il thread di aggiornamento aggiungerà un blocco di richieste di lavoro alle code dei thread di lavoro. L'aggiunta all'elenco una sola volta per fotogramma garantisce che il thread di aggiornamento riduca al minimo il sovraccarico di sincronizzazione. Ogni thread di lavoro esegue il pull delle assegnazioni dalla coda di lavoro nel modo più rapido possibile, usando un ciclo simile al seguente:

for(;;)
{
    while( WorkQueueNotEmpty() )
    {
        RemoveWorkItemFromWorkQueue();
        ProcessWorkItem();
        PutResultInDataQueue();
    }
    WaitForSingleObject( hWorkSemaphore ); 
}

Poiché i dati passano dai thread di aggiornamento ai thread di lavoro e quindi al thread di rendering, può verificarsi un ritardo di tre o più fotogrammi prima che alcune azioni lo facciano sullo schermo. Tuttavia, se si assegnano attività a tolleranza di latenza ai thread di lavoro, questo non dovrebbe essere un problema.

Una progettazione alternativa sarebbe quella di avere diversi thread di lavoro tutti di disegno dalla stessa coda di lavoro. In questo modo, il bilanciamento del carico automatico e renderebbe più probabile che tutti i thread di lavoro rimangano occupati.

Il thread di aggiornamento del gioco deve prestare attenzione a non dare troppo lavoro ai thread di lavoro, altrimenti le code di lavoro possono aumentare continuamente. La modalità di gestione del thread di aggiornamento dipende dal tipo di attività eseguite dai thread di lavoro.

Multithreading simultaneo e numero di thread

Tutti i thread non vengono creati in modo uguale. Due thread hardware potrebbero essere su chip separati, sullo stesso chip o anche sullo stesso core. La configurazione più importante per i programmatori di giochi da tenere presente è due thread hardware su un core: simultaneo multithreading (SMT) o Hyper-Threading Technology (tecnologia HT).

I thread SMT o HT Technology condividono le risorse del core CPU. Poiché condividono le unità di esecuzione, la velocità massima dall'esecuzione di due thread invece di una è in genere dal 10 al 20% anziché dal 100% possibile da due thread hardware indipendenti.

In modo più significativo, i thread SMT o HT Technology condividono le istruzioni e le cache dei dati L1. Se i modelli di accesso alla memoria non sono compatibili, possono finire per combattere la cache e causare molti mancati riscontri nella cache. Nel peggiore dei casi, le prestazioni totali per il core CPU possono effettivamente diminuire quando viene eseguito un secondo thread. Su Xbox 360 questo è un problema abbastanza semplice. La configurazione di Xbox 360 è nota, tre core CPU ciascuno con due thread hardware, e gli sviluppatori assegnano i thread software a thread CPU specifici e possono misurare se la progettazione del threading offre prestazioni aggiuntive.

In Windows la situazione è più complicata. Il numero di thread e la relativa configurazione variano da computer a computer e determinare la configurazione è complicata. La funzione GetLogicalProcessorInformation fornisce informazioni sulla relazione tra diversi thread hardware e questa funzione è disponibile in Windows Vista, Windows 7 e Windows XP SP3. Pertanto, per ora è necessario usare l'istruzione CPUID e gli algoritmi forniti da Intel e AMD per decidere quanti thread "reali" sono disponibili. Per altre informazioni, vedere i riferimenti.

L'esempio CoreDetection in DirectX SDK contiene codice di esempio che usa la funzione GetLogicalProcessorInformation o l'istruzione CPUID per restituire la topologia del core CPU. L'istruzione CPUID viene usata se GetLogicalProcessorInformation non è supportato nella piattaforma corrente. CoreDetection è disponibile nelle posizioni seguenti:

Origine:

Root\Samples\C++\Misc\CoreDetection di DirectX SDK

Eseguibile:

DirectX SDK root\Samples\C++\Misc\Bin\CoreDetection.exe

Il presupposto più sicuro consiste nell'avere non più di un thread a elevato utilizzo di CPU per ogni core CPU. Avere thread a elevato utilizzo di CPU rispetto ai core CPU offre pochi o nessun vantaggio e comporta un sovraccarico e una complessità aggiuntivi di thread aggiuntivi.

Creazione di thread

La creazione di thread è un'operazione abbastanza semplice, ma esistono molti errori potenziali. Il codice seguente illustra il modo corretto per creare un thread, attendere che venga terminato e quindi pulire.

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;
}

Quando si crea un thread, è possibile specificare le dimensioni dello stack per il thread figlio o specificare zero, nel qual caso il thread figlio erediterà le dimensioni dello stack del thread padre. In Xbox 360, dove gli stack vengono sottoposti a commit completo all'avvio del thread, specificando zero può sprecare memoria significativa, perché molti thread figlio non avranno bisogno di tanto stack quanto l'elemento padre. Su Xbox 360 è anche importante che le dimensioni dello stack siano un multiplo di 64 KB.

Se si usa la funzione CreateThread per creare thread, il runtime C/C++ (CRT) non verrà inizializzato correttamente in Windows. È consigliabile usare invece la funzione _beginthreadex CRT.

Il valore restituito da CreateThread o _beginthreadex è un handle di thread. Questo thread può essere usato per attendere che il thread figlio venga terminato, che è molto più semplice e molto più efficiente rispetto alla rotazione in un ciclo che controlla lo stato del thread. Per attendere che il thread venga terminato, è sufficiente chiamare WaitForSingleObject con l'handle del thread.

Le risorse per il thread non verranno liberate finché il thread non viene terminato e l'handle del thread è stato chiuso. Pertanto, è importante chiudere l'handle di thread con CloseHandle al termine dell'operazione. Se il thread verrà terminato con WaitForSingleObject, assicurarsi di non chiudere l'handle fino al completamento dell'attesa.

In Xbox 360 devi assegnare in modo esplicito thread software a un thread hardware specifico usando XSetThreadProcessor. In caso contrario, tutti i thread figlio rimarranno nello stesso thread hardware dell'elemento padre. In Windows è possibile usare SetThreadAffinityMask per suggerire vivamente al sistema operativo su cui deve essere eseguito il thread hardware del thread. Questa tecnica dovrebbe in genere essere evitata in Windows perché non si sa quali altri processi potrebbero essere in esecuzione nel sistema. In genere è preferibile consentire all'utilità di pianificazione di Windows di assegnare i thread ai thread hardware inattive.

La creazione di thread è un'operazione costosa. I thread devono essere creati e distrutti raramente. Se si vuole creare ed eliminare spesso i thread, usare un pool di thread che attendono il lavoro.

Sincronizzazione dei thread

Affinché più thread funzionino insieme, è necessario essere in grado di sincronizzare thread, passare messaggi e richiedere l'accesso esclusivo alle risorse. Windows e Xbox 360 sono dotati di una vasta gamma di primitive di sincronizzazione. Per informazioni dettagliate su queste primitive di sincronizzazione, vedere la documentazione della piattaforma.

Accesso esclusivo

Ottenere l'accesso esclusivo a una risorsa, a una struttura di dati o a un percorso di codice è una necessità comune. Un'opzione per ottenere l'accesso esclusivo è un mutex, il cui utilizzo tipico è illustrato qui.

// 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 );

Le sezioni critiche hanno una semantica simile ai mutex, ma possono essere usate per la sincronizzazione solo all'interno di un processo, non tra processi. Il loro vantaggio principale è che vengono eseguiti circa venti volte più velocemente dei mutex.

Eventi

Se due thread, ad esempio un thread di aggiornamento e un thread di rendering, usano una coppia di buffer di descrizione di rendering, è necessario un modo per indicare quando vengono eseguiti con il buffer specifico. Questa operazione può essere eseguita associando un evento (allocato a CreateEvent) a ogni buffer. Quando un thread viene eseguito con un buffer, può usare SetEvent per segnalarlo e quindi chiamare WaitForSingleObject sull'evento dell'altro buffer. Questa tecnica estrapola facilmente il triplo buffer delle risorse.

Semafori

Un semaforo viene usato per controllare il numero di thread in esecuzione e viene comunemente usato per implementare le code di lavoro. Un thread aggiunge lavoro a una coda e usa ReleaseSemaphore ogni volta che aggiunge un nuovo elemento alla coda. Ciò consente il rilascio di un thread di lavoro dal pool di thread in attesa. I thread di lavoro chiamano semplicemente WaitForSingleObject e, quando viene restituito, sanno che nella coda è presente un elemento di lavoro. Inoltre, è necessario usare una sezione critica o un'altra tecnica di sincronizzazione per garantire l'accesso sicuro alla coda di lavoro condivisa.

Evitare SuspendThread

In alcuni casi, quando si vuole che un thread interrompa le operazioni, è tentata l'uso di SuspendThread invece delle primitive di sincronizzazione corrette. Questa è sempre una cattiva idea e può facilmente portare a deadlock e altri problemi. SuspendThread interagisce anche male con il debugger di Visual Studio. Evitare SuspendThread. Usare invece WaitForSingleObject.

WaitForSingleObject e WaitForMultipleObjects

La funzione WaitForSingleObject è la funzione di sincronizzazione più comunemente usata. Tuttavia, a volte si vuole che un thread attenda fino a quando diverse condizioni non vengono soddisfatte contemporaneamente o fino a quando non viene soddisfatta una delle condizioni di un set di condizioni. In questo caso, è consigliabile usare WaitForMultipleObjects.

Funzioni interlocked e programmazione senza blocchi

Esiste una famiglia di funzioni per l'esecuzione di semplici operazioni thread-safe senza usare blocchi. Si tratta della famiglia interlocked di funzioni, ad esempio InterlockedIncrement. Queste funzioni, oltre ad altre tecniche che usano un'attenta impostazione dei flag, sono note come programmazione senza blocchi. La programmazione senza blocchi può essere estremamente difficile da eseguire correttamente ed è sostanzialmente più difficile su Xbox 360 rispetto a Windows.

Per altre informazioni sulla programmazione senza blocchi, vedi Considerazioni sulla programmazione senza blocchi per Xbox 360 e Microsoft Windows.

Riduzione al minimo della sincronizzazione

Alcuni metodi di sincronizzazione sono più veloci di altri. Tuttavia, invece di ottimizzare il codice scegliendo le tecniche di sincronizzazione più veloci possibili, in genere è preferibile sincronizzare meno spesso. Questa operazione è più veloce rispetto alla sincronizzazione troppo frequente e rende più semplice il debug del codice.

Alcune operazioni, ad esempio l'allocazione di memoria, potrebbero dover usare primitive di sincronizzazione per funzionare correttamente. Di conseguenza, l'esecuzione di allocazioni frequenti dall'heap condiviso predefinito comporterà una sincronizzazione frequente, che perderà alcune prestazioni. Evitare allocazioni frequenti o usare heap per thread (usando HEAP_NO_edizione Standard RIALIZE se si usa HeapCreate) può evitare questa sincronizzazione nascosta.

Un'altra causa della sincronizzazione nascosta è D3DCREATE_MULTITHREADED, che fa sì che D3D in Windows usi la sincronizzazione su molte operazioni. Il flag viene ignorato in Xbox 360.

I dati per thread, noti anche come archiviazione locale del thread, possono essere un modo importante per evitare la sincronizzazione. Visual C++ consente di dichiarare le variabili globali come per thread con la sintassi __declspec(thread).

__declspec( thread ) int tls_i = 1;

In questo modo, ogni thread nel processo ha la propria copia di tls_i, a cui è possibile fare riferimento in modo sicuro ed efficiente senza richiedere la sincronizzazione.

La tecnica __declspec(thread) non funziona con DLL caricate dinamicamente. Se si usano DLL caricate dinamicamente, è necessario usare la famiglia di funzioni TLSAlloc per implementare l'archiviazione locale del thread.

Eliminazione definitiva di thread

L'unico modo sicuro per eliminare definitivamente un thread consiste nell'uscire dal thread stesso, restituendo dalla funzione thread principale o facendo in modo che il thread chiami ExitThread o _endthreadex. Se un thread viene creato con _beginthreadex, deve usare _endthreadex o restituire dalla funzione thread principale, poiché l'uso di ExitThread non libera correttamente le risorse CRT. Non chiamare mai la funzione TerminateThread , perché il thread non verrà pulito correttamente. I fili dovrebbero sempre commettere suicidio- non dovrebbero mai essere uccisi.

OpenMP

OpenMP è un'estensione del linguaggio per l'aggiunta di multithreading al programma usando pragmas per guidare il compilatore in cicli di parallelizzazione. OpenMP è supportato da Visual C++ 2005 in Windows e Xbox 360 e può essere usato insieme alla gestione manuale dei thread. OpenMP può essere un modo pratico per eseguire parti multithread del codice, ma è improbabile che sia la soluzione ideale, soprattutto per i giochi. OpenMP può essere più applicabile alle attività di produzione con esecuzione più lunga, ad esempio l'elaborazione dell'arte e altre risorse. Per altre informazioni, vedere la documentazione di Visual C++ o passare al sito Web OpenMP.

Raccolta informazioni utente

La profilatura multithreading è importante. È facile finire con lunghe stalle in cui i thread sono in attesa l'uno sull'altro. Questi stalli possono essere difficili da trovare e diagnosticare. Per identificarli, è consigliabile aggiungere la strumentazione alle chiamate di sincronizzazione. Un profiler di campionamento può anche aiutare a identificare questi problemi perché può registrare informazioni di temporizzazione senza modificarle in modo sostanziale.

Temporizzazione

L'istruzione rdtsc è un modo per ottenere informazioni precise sulla tempistica in Windows. Sfortunatamente, rdtsc presenta più problemi che lo rendono una scelta scadente per il titolo di spedizione. I contatori rdtsc non sono necessariamente sincronizzati tra CPU, quindi quando il thread si sposta tra thread hardware, è possibile che si verifichino grandi differenze positive o negative.The rdtsc counters are not necessariamente synchronized between CPU, so when your thread move between hardware threads you may get large positive or negative differences. A seconda delle impostazioni di risparmio energia, la frequenza con cui i contatori rdtsc incrementi possono cambiare anche durante l'esecuzione del gioco. Per evitare queste difficoltà, è consigliabile preferire QueryPerformanceCounter e QueryPerformanceFrequency per tempi di precisione elevata nel gioco di spedizione. Per altre informazioni sulla tempistica, vedi Tempi del gioco e processori multicore.

Debug

Visual Studio supporta completamente il debug multithreading per Windows e Xbox 360. La finestra thread di Visual Studio consente di passare da un thread all'altro per visualizzare i diversi stack di chiamate e variabili locali. La finestra thread consente anche di bloccare e scongelare determinati thread.

In Xbox 360 puoi usare la meta-variabile @hwthread nella finestra espressioni di controllo per visualizzare il thread hardware in cui è in esecuzione il thread software attualmente selezionato.

La finestra thread è più semplice da usare se si assegna un nome significativo ai thread. Visual Studio e altri debugger Microsoft consentono di denominare i thread. Implementare la funzione SetThreadName seguente e chiamarla da ogni thread all'avvio.

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");

Il debugger del kernel (KD) e WinDBG supportano anche il debug multithreading.

Test in corso

La programmazione multithreading può essere complessa e alcuni bug multithreading vengono visualizzati solo raramente, rendendo difficile trovare e correggere. Uno dei modi migliori per scaricarli è quello di testare su un'ampia gamma di computer, in particolare quelli con quattro o più processori. Il codice multithreading che funziona perfettamente in un computer a thread singolo può non riuscire immediatamente in un computer a quattro processori. Le caratteristiche di prestazioni e temporizzazione delle CPU AMD e Intel possono variare notevolmente, quindi assicurarsi di testare su computer multiprocessore basati su CPU di entrambi i fornitori.

Miglioramenti di Windows Vista e Windows 7

Per i giochi destinati alle versioni più recenti di Windows, esistono diverse API che possono semplificare la creazione di applicazioni multithreading scalabili. Ciò è particolarmente vero con la nuova API ThreadPool e alcune primitive di sincronizzazione aggiuntive (variabili di condizione, blocco di lettura/scrittura sottile e inizializzazione monouso). Per una panoramica di queste tecnologie, vedere gli articoli di MSDN Magazine seguenti:

Le applicazioni che usano le funzionalità direct3D 11 in questi sistemi operativi possono anche sfruttare la nuova progettazione per la creazione di oggetti simultanei e gli elenchi di comandi di contesto posticipati per una migliore scalabilità per il rendering multithreading.

Riepilogo

Con un'attenta progettazione che riduce al minimo le interazioni tra thread, è possibile ottenere notevoli miglioramenti delle prestazioni dalla programmazione multithreading senza aggiungere una complessità eccessiva al codice. Questo permetterà al codice del gioco di guidare la prossima ondata di miglioramenti del processore e offrire esperienze di gioco sempre più accattivanti.

Riferimenti