Quanti thread dovrei avere e per cosa?


81

Dovrei avere thread separati per rendering e logica, o anche di più?

Sono consapevole dell'immenso calo delle prestazioni causato dalla sincronizzazione dei dati (per non parlare di eventuali blocchi di mutex).

Ho pensato di portarlo all'estremo e fare discussioni per concepire ogni sottosistema concepibile. Ma sono preoccupato che possa rallentare anche le cose. (Ad esempio, è ragionevole separare il thread di input dal rendering o dai thread della logica di gioco?) La sincronizzazione dei dati richiesta lo renderebbe inutile o addirittura più lento?


6
quale piattaforma? PC, console NextGen, smartphone?
Ellis,

C'è una cosa a cui posso pensare che richiederebbe il multi-threading; networking.
Sapone,

abbandonare le esagerazioni, non c'è un "immenso" rallentamento quando sono coinvolti i blocchi. questa è una leggenda urbana e un pregiudizio.
v.oddou,

Risposte:


61

L'approccio comune per trarre vantaggio da più core è, francamente, semplicemente fuorviato. Separare i sottosistemi in thread diversi dividerà effettivamente parte del lavoro su più core, ma presenta alcuni problemi importanti. Innanzitutto, è molto difficile lavorare con. Chi vuole andare in giro con lucchetti, sincronizzazione, comunicazione e cose quando invece potrebbero semplicemente scrivere codice di rendering o fisica? In secondo luogo, l'approccio in realtà non si ingrandisce. Nella migliore delle ipotesi, questo ti consentirà di trarre vantaggio da forse tre o quattro core, e questo è se sai davvero cosa stai facendo. Ci sono così tanti sottosistemi in un gioco e di quelli ce ne sono ancora meno che occupano grossi pezzi di tempo della CPU. Ci sono un paio di buone alternative che conosco.

Uno è avere un thread principale insieme a un thread di lavoro per ogni CPU aggiuntiva. Indipendentemente dal sottosistema, il thread principale delega le attività isolate ai thread di lavoro tramite una sorta di coda (e); questi compiti possono anche creare altri compiti. L'unico scopo dei thread di lavoro è quello di prendere ogni attività dalla coda una alla volta ed eseguirle. La cosa più importante, tuttavia, è che non appena un thread necessita del risultato di un'attività, se l'attività viene completata può ottenere il risultato e, in caso contrario, può rimuovere in sicurezza l'attività dalla coda e andare avanti ed eseguirla compito stesso. Cioè, non tutte le attività finiranno per essere pianificate in parallelo tra loro. Avere più compiti di quanti possano essere eseguiti in parallelo è un benecosa in questo caso; significa che è probabile che si ridimensioni quando aggiungi più core. Un aspetto negativo di questo è che richiede molto lavoro in anticipo per progettare una coda e un ciclo di lavoro decenti a meno che tu non abbia accesso a una libreria o al runtime della lingua che già ti fornisce questo. La parte più difficile è assicurarsi che i tuoi compiti siano veramente isolati e sicuri per i thread, e assicurarti che i tuoi compiti siano in una felice via di mezzo tra grana grossa e grana fine.

Un'altra alternativa ai thread del sottosistema è quella di parallelizzare ciascun sottosistema in isolamento. Cioè, invece di eseguire il rendering e la fisica nei propri thread, scrivere il sottosistema di fisica per utilizzare tutti i core contemporaneamente, scrivere il sottosistema di rendering per utilizzare tutti i core contemporaneamente, quindi fare in modo che i due sistemi vengano eseguiti in sequenza (o interfogliati, a seconda di altri aspetti dell'architettura di gioco). Ad esempio, nel sottosistema di fisica potresti prendere tutte le masse di punti del gioco, dividerle tra i tuoi core e quindi far aggiornare tutti i core contemporaneamente. Ogni core può quindi lavorare sui tuoi dati in loop stretti con una buona località. Questo stile di parallelismo di blocco è simile a quello che fa una GPU. La parte più difficile qui è assicurarsi di dividere il lavoro in blocchi a grana fine in modo tale da dividerlo uniformementein realtà si traduce in una uguale quantità di lavoro su tutti i processori.

Tuttavia, a volte è più semplice, a causa della politica, del codice esistente o di altre circostanze frustranti, dare a ciascun sottosistema un filo. In tal caso, è meglio evitare di creare più thread del sistema operativo rispetto ai core per carichi di lavoro pesanti della CPU (se si dispone di un runtime con thread leggeri che si bilanciano tra i core, questo non è un grosso problema). Inoltre, evitare comunicazioni eccessive. Un bel trucco è provare il pipelining; ogni sottosistema principale può lavorare su uno stato di gioco diverso alla volta. Il pipelining riduce la quantità di comunicazione necessaria tra i sottosistemi poiché non hanno tutti bisogno di accedere agli stessi dati contemporaneamente e può anche annullare alcuni dei danni causati da strozzature. Per esempio, se il sottosistema di fisica tende a richiedere molto tempo per essere completato e il sottosistema di rendering finisce sempre in attesa, il frame rate assoluto potrebbe essere maggiore se si esegue il sottosistema di fisica per il fotogramma successivo mentre il sottosistema di rendering funziona ancora sul precedente telaio. In effetti, se si hanno tali colli di bottiglia e non è possibile rimuoverli in altro modo, il pipelining può essere il motivo più legittimo per preoccuparsi dei thread del sottosistema.


"non appena un thread ha bisogno del risultato di un'attività, se l'attività è completata può ottenere il risultato e, in caso contrario, può rimuovere in modo sicuro l'attività dalla coda e andare avanti ed eseguire l'attività stessa". Stai parlando di un'attività generata dallo stesso thread? In tal caso, non avrebbe più senso se quell'attività viene eseguita dal thread che ha generato l'attività stessa?
jmp97,

cioè il thread potrebbe, senza pianificare l'attività, eseguire immediatamente quell'attività.
jmp97,

3
Il punto è che il thread non sa necessariamente in anticipo se sarebbe meglio eseguire l'attività in parallelo o meno. L'idea è quella di stimolare in modo speculativo il lavoro che alla fine dovrai fare, e se un altro thread si trova inattivo, può andare avanti e fare questo lavoro per te. Se questo non accade nel momento in cui hai bisogno del risultato, puoi semplicemente estrarre l'attività dalla coda. Questo schema serve per bilanciare dinamicamente un carico di lavoro su più core anziché staticamente.
Jake McArthur,

Ci scusiamo per aver impiegato così tanto tempo per tornare a questa discussione. Non sto prestando attenzione a Gamedev ultimamente. Questa è probabilmente la risposta migliore, schietta ma al punto ed estesa.
j riv

1
Hai ragione nel senso che ho trascurato di parlare di carichi di lavoro pesanti I / O. La mia interpretazione della domanda era che riguardava solo carichi di lavoro pesanti per la CPU.
Jake McArthur,

30

Ci sono un paio di cose da considerare. La route thread per sottosistema è facile da pensare poiché la separazione del codice è piuttosto evidente fin dall'inizio. Tuttavia, a seconda della quantità di intercomunicazione di cui hanno bisogno i sottosistemi, la comunicazione tra thread potrebbe davvero compromettere le prestazioni. Inoltre, questo si riduce solo a N core, dove N è il numero di sottosistemi astratti in thread.

Se stai solo cercando di eseguire il multithreading di un gioco esistente, questo è probabilmente il percorso di minor resistenza. Tuttavia, se stai lavorando su alcuni sistemi di motori di basso livello che potrebbero essere condivisi tra diversi giochi o progetti, prenderei in considerazione un altro approccio.

Può volerci un po 'di torsione, ma se riesci a spezzare le cose come una fila di lavori con una serie di thread di lavoro, nel lungo periodo si ridimensionerà molto meglio. Man mano che le chip più recenti e più grandi escono con un nucleo di gazillion, le prestazioni del tuo gioco aumenteranno di pari passo, semplicemente accendendo più thread di lavoro.

Quindi, fondamentalmente, se stai cercando di sfruttare un po 'di parallelismo con un progetto esistente, parallelizzerei tutti i sottosistemi. Se stai costruendo un nuovo motore da zero con in mente la scalabilità parallela, esaminerei una coda di lavoro.


Il sistema che citi è molto simile a un sistema di programmazione menzionato nella risposta data dall'Altro James, ancora buoni dettagli in quell'area, quindi +1 in quanto si aggiunge alla discussione.
James,

3
sarebbe utile una wiki della comunità su come impostare una coda di lavoro e i thread di lavoro.
bot_bot,

23

Questa domanda non ha una risposta migliore, poiché dipende da ciò che stai cercando di realizzare.

L'xbox ha tre core e può gestire alcuni thread prima che l'overhead del cambio di contesto diventi un problema. Il pc può occuparsene un po 'di più.

Molti giochi sono stati in genere a thread singolo per facilitare la programmazione. Questo va bene per la maggior parte dei giochi personali. L'unica cosa per cui dovresti probabilmente avere un altro thread è Networking e Audio.

Unreal ha un thread di gioco, thread di rendering, thread di rete e thread audio (se ricordo bene). Questo è abbastanza standard per molti motori di generazione attuale, sebbene essere in grado di supportare un thread di rendering separato può essere una seccatura e richiede molte basi.

Il motore idTech5 sviluppato per Rage attualmente utilizza un numero qualsiasi di thread e lo fa suddividendo le attività di gioco in "lavori" che vengono elaborati con un sistema di tasking. Il loro obiettivo esplicito è di far sì che il loro motore di gioco si ridimensioni bene quando salta il numero di core sul sistema di gioco medio.

La tecnologia che uso (e ho scritto) ha un thread separato per Networking, Input, Audio, Rendering e Scheduling. Ha quindi un numero qualsiasi di thread che possono essere utilizzati per eseguire attività di gioco e questo è gestito dal thread di pianificazione. Un sacco di lavoro è andato in ricevendo tutti i fili per giocare bene con l'altro, ma sembra funzionare bene e ottenere molto buon uso dei sistemi multicore, quindi forse è missione compiuta (per ora; potrei abbattere audio / networking / input funziona solo in "attività" che i thread di lavoro possono aggiornare).

Dipende davvero dal tuo obiettivo finale.


+1 per la menzione di un sistema di programmazione .. di solito un buon posto per centrare la comunicazione thread / sistema :)
James

Perché il voto negativo, downvoter?
jcora,

12

Un thread per sottosistema è la strada sbagliata da percorrere. Improvvisamente, la tua app non si ridimensionerà perché alcuni sottosistemi richiedono molto più di altri. Questo era l'approccio di threading adottato da Supreme Commander e non si espandeva oltre due core perché avevano solo due sottosistemi che occupavano una notevole quantità di rendering della CPU e logica fisica / di gioco, anche se avevano 16 thread, gli altri thread ammontava a malapena a qualsiasi lavoro e, di conseguenza, il gioco si ridimensionava solo a due core.

Quello che dovresti fare è usare qualcosa chiamato pool di thread. Ciò rispecchia in qualche modo l'approccio adottato sulle GPU - ovvero, pubblichi il lavoro e qualsiasi thread disponibile semplicemente arriva e lo fa, e poi ritorna in attesa di lavoro - pensalo come un buffer ad anello, di thread. Questo approccio ha il vantaggio del ridimensionamento N-core ed è molto efficace nel ridimensionamento per conteggi di core sia bassi che alti. Lo svantaggio è che è piuttosto difficile gestire la proprietà del thread per questo approccio, poiché è impossibile sapere quale thread sta facendo ciò che funziona in un determinato momento, quindi è necessario bloccare i problemi di proprietà molto strettamente. Inoltre rende molto difficile utilizzare tecnologie come Direct3D9 che non supportano più thread.

I pool di thread sono molto difficili da usare, ma offrono i migliori risultati possibili. Se hai bisogno di un ridimensionamento estremamente buono o hai un sacco di tempo per lavorarci sopra, usa un pool di thread. Se stai cercando di introdurre il parallelismo in un progetto esistente con problemi di dipendenza sconosciuti e tecnologie a thread singolo, questa non è la soluzione per te.


Giusto per essere un po 'più precisi: le GPU non usano pool di thread ma lo scheduler dei thread è implementato nell'hardware, il che rende molto economico creare nuovi thread e cambiare thread, al contrario delle CPU dove la creazione di thread e gli switch di contesto sono costosi. Vedere la Guida del programmatore Nvidias CUDA per esempio.
Nils,

2
+1: la migliore risposta qui. Userei anche più costrutti astratti dei threadpools (ad esempio, code di lavoro e lavoratori) se il tuo framework lo consente. È molto più facile pensare / programmare in questi termini rispetto ai thread / blocchi / etc puri. Inoltre: dividere il gioco in rendering, logica, ecc. Non ha senso, poiché il rendering deve attendere il completamento della logica. Piuttosto creare lavori che possono essere effettivamente eseguiti in parallelo (ad esempio: calcolare l'intelligenza artificiale per un npc per il fotogramma successivo).
Dave O.

@DaveO. Il tuo punto "Plus" è così, così vero.
Ingegnere,

11

Hai ragione a dire che la parte più critica è evitare la sincronizzazione laddove possibile. Ci sono alcuni modi per raggiungere questo obiettivo.

  1. Conosci i tuoi dati e archiviali in memoria in base alle tue esigenze di elaborazione. Ciò consente di pianificare calcoli paralleli senza la necessità di sincronizzazione. Sfortunatamente, questo è il più delle volte abbastanza difficile da ottenere poiché spesso si accede ai dati da sistemi diversi in tempi imprevedibili.

  2. Definire tempi di accesso chiari per i dati. È possibile separare il segno di spunta principale in fasi x. Se sei sicuro che Thread X legge i dati solo in una fase specifica, sai anche che questi dati possono essere modificati da altri thread in una fase diversa.

  3. Doppio buffer dei dati. Questo è l'approccio più semplice, ma aumenta la latenza, poiché Thread X sta lavorando con i dati dell'ultimo frame, mentre Thread Y sta preparando i dati per il frame successivo.

La mia esperienza personale dimostra che i calcoli a grana fine sono il modo più efficace, in quanto possono scalare molto meglio di soluzioni basate su un sottosistema. Se si esegue il threading dei sottosistemi, il frame-time verrà associato al sottosistema più costoso. Questo può portare a tutti i thread tranne uno inattivo fino a quando il costoso sottosistema non ha finalmente finito di funzionare. Se sei in grado di separare gran parte del tuo gioco in piccole attività, queste attività possono essere programmate di conseguenza per evitare core inattivi. Ma questo è qualcosa che è difficile da realizzare se hai già una grande base di codice.

Per prendere in considerazione alcuni vincoli hardware, dovresti cercare di non sottoscrivere mai più l'hardware. Con oversubscribe, intendo avere più thread software rispetto ai thread hardware della tua piattaforma. Soprattutto sulle architetture PPC (Xbox 360, PS3) un cambio di attività è davvero costoso. Ovviamente va bene se hai un numero eccessivo di thread sottoscritti che vengono attivati ​​solo per un breve periodo di tempo (una volta un frame, ad esempio) Se scegli come target il PC, dovresti tenere presente che il numero di core (o meglio HW -Threads) è in costante crescita, quindi vorrai trovare una soluzione scalabile, che sfrutti la potenza aggiuntiva della CPU. Quindi, in quest'area, dovresti provare a progettare il tuo codice il più possibile in base alle attività.


3

Regola generale per il threading di un'applicazione: 1 thread per CPU Core. Su un PC quad core che significa 4. Come notato, l'XBox 360 ha comunque 3 core ma 2 thread hardware ciascuno, quindi 6 thread in questo caso. Su un sistema come la PS3 ... beh, buona fortuna su quello :) Le persone stanno ancora cercando di capirlo.

Vorrei suggerire di progettare ogni sistema come un modulo autonomo che è possibile eseguire il thread se si desidera. Questo di solito significa avere percorsi di comunicazione ben definiti tra il modulo e il resto del motore. Mi piacciono in particolare i processi di sola lettura come il rendering e l'audio, nonché i processi "ci siamo ancora" come la lettura dell'input del lettore per le cose da escludere. Toccando la risposta data da AttackingHobo, quando si esegue un rendering di 30-60 fps, se i dati sono obsoleti di 1/30 di 1/1/60 di secondo, non toglie nulla alla sensazione di risposta del gioco. Ricorda sempre che la differenza principale tra software applicativo e videogiochi sta facendo tutto 30-60 volte al secondo. In quella stessa nota però,

Se progetti i sistemi del tuo motore abbastanza bene, ognuno di essi può essere spostato da un thread all'altro per bilanciare il carico del motore in modo più appropriato in base al gioco e simili. In teoria, potresti anche utilizzare il tuo motore in un sistema distribuito, se necessario, dove sistemi informatici completamente separati eseguono ciascun componente.


2
Xbox360 ha 2 hardwarethreads per core, quindi il numero ottimale di thread è 6.
DarthCoder

Ah, +1 :) Sono sempre stato limitato alle aree di rete di 360 e ps3, hehe :)
James

0

Creo un thread per core logico (meno uno, per tenere conto del thread principale, che per inciso è responsabile del rendering, ma che funge anche da thread del lavoratore).

Colleziono eventi del dispositivo di input in tempo reale in un frame, ma non li applico fino alla fine del frame: avranno effetto nel frame successivo. E utilizzo una logica simile per il rendering (vecchio stato) rispetto all'aggiornamento (nuovo stato).

Uso gli eventi atomici per rinviare le operazioni non sicure fino a tardi nello stesso frame e utilizzo più di una coda di eventi (coda di lavoro) al fine di implementare una barriera di memoria che offre una garanzia ben definita sull'ordine delle operazioni, senza bloccare o attendere (blocca le code simultanee libere in ordine di priorità del lavoro).

È interessante notare che qualsiasi lavoro può emettere sottoprocessi (che sono più fini e si avvicinano all'atomicità) alla stessa coda di priorità o ad uno più alto (servito più avanti nel frame).

Dato che ho tre di queste code, tutti i thread tranne uno possono potenzialmente bloccarsi esattamente tre volte per frame (in attesa che altri thread completino tutti i lavori in sospeso emessi al livello di priorità corrente).

Questo sembra un livello accettabile di inattività del thread!


Il mio frame inizia con MAIN rendering dello STATO VECCHIO dal passaggio di aggiornamento del frame precedente, mentre tutti gli altri thread iniziano immediatamente a calcolare lo stato del frame SUCCESSIVO, sto solo usando Eventi per raddoppiare le modifiche dello stato del buffer fino a un punto nel frame in cui nessuno sta più leggendo .
Omero,

0

Di solito uso un thread principale (ovviamente) e aggiungo un thread ogni volta che noto un calo delle prestazioni di circa il 10-20 percento. Per perdere una goccia del genere uso gli strumenti di performance di Visual Studio. Gli eventi comuni sono (un) caricamento di alcune aree della mappa o eseguire alcuni calcoli pesanti.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.