Come posso rendere meno ingombrante il passaggio dei messaggi tra i thread in un motore multithread?


18

Il motore C ++ su cui sto lavorando attualmente è suddiviso in diversi thread di grandi dimensioni: Generazione (per la creazione del mio contenuto procedurale), Gameplay (per AI, script, simulazione), Fisica e Rendering.

I thread comunicano tra loro attraverso piccoli oggetti messaggio, che passano da un thread all'altro. Prima di fare un passo, un thread elabora tutti i suoi messaggi in arrivo: gli aggiornamenti si trasformano, aggiungono e rimuovono oggetti, ecc. A volte un thread (Generation) crea qualcosa (Art) e lo passa a un altro thread (Rendering) per la proprietà permanente.

All'inizio del processo e ho notato un paio di cose:

  1. Il sistema di messaggistica è ingombrante. Creare un nuovo tipo di messaggio significa sottoclassare la classe Message di base, creare un nuovo enum per il suo tipo e scrivere la logica su come i thread dovrebbero interpretare il nuovo tipo di messaggio. È un ostacolo per lo sviluppo ed è soggetto a errori di battitura. (Sidenote- lavorando su questo mi fa apprezzare quanto possano essere grandi le lingue dinamiche!)

    C'è un modo migliore per farlo? Dovrei usare qualcosa come boost :: bind per rendere questo automatico? Sono preoccupato che se lo faccio perdo la capacità di dire, ordinare i messaggi in base al tipo o qualcosa del genere. Non sono sicuro se quel tipo di gestione sarà persino necessario.

  2. Il primo punto è importante perché questi thread comunicano molto. La creazione e il passaggio di messaggi è una parte importante del far accadere le cose. Vorrei semplificare quel sistema, ma anche essere aperto ad altri paradigmi che potrebbero essere altrettanto utili. Ci sono diversi progetti multithread a cui dovrei pensare per facilitare questo?

    Ad esempio, ci sono alcune risorse che sono scritte raramente, ma spesso letti da più thread. Dovrei essere aperto all'idea di avere dati condivisi, protetti da mutex, a cui tutti i thread possano accedere?

Questa è la mia prima volta a progettare qualcosa con il multithreading in mente da zero. In questa fase iniziale penso davvero che stia andando molto bene (considerando), ma sono preoccupato per il ridimensionamento e la mia efficienza nell'implementazione di nuove cose.


6
Non c'è davvero un'unica domanda diretta qui, e come tale questo post non è adatto allo stile di domande e risposte di questo sito. Ti consiglierei di dividere il tuo post in post distinti, uno per domanda, e di riorientare le domande in modo che stiano ponendo un problema specifico che stai effettivamente avendo invece di una vaga raccolta di suggerimenti o consigli.

2
Se vuoi impegnarti in una conversazione più generale, ti consiglio di riprovare questo post nei forum su gamedev.net . Come ha detto Josh, poiché la tua "domanda" non è una singola domanda specifica, sarebbe abbastanza difficile adattarsi al formato StackExchange.
Cypher,

Grazie per il feedback ragazzi! Speravo in qualche modo che qualcuno con più conoscenze potesse avere una singola risorsa / esperienza / paradigma che potesse affrontare diversi dei miei problemi contemporaneamente. Ho la sensazione che una grande idea possa sintetizzare i miei vari problemi in una cosa che mi manca, e pensavo che qualcuno con più esperienza di quella che avrei potuto riconoscere che ... Ma forse no, e comunque- punti presi !
Raptormeat

Ho rinominato il tuo titolo in modo più specifico per il passaggio dei messaggi, poiché le domande di tipo "suggerimenti" implicano che non esiste un problema specifico da risolvere (e quindi in questi giorni chiuderei come "non una vera domanda").
Tetrad

Sei sicuro di aver bisogno di thread separati per la fisica e il gameplay? Quei due sembrano essere molto intrecciati. Inoltre, è difficile sapere come offrire consigli senza sapere come ciascuno di essi comunica e con chi.
Nicol Bolas,

Risposte:


10

Per il tuo problema più ampio, considera di cercare di trovare il modo di ridurre il più possibile la comunicazione tra thread. È meglio evitare del tutto i problemi di sincronizzazione, se puoi. Ciò può essere ottenuto mediante il doppio buffering dei dati, introducendo una latenza a singolo aggiornamento ma facilitando notevolmente l'attività di lavoro con i dati condivisi.

A parte questo, hai considerato di non eseguire il threading per sottosistema, ma invece di utilizzare la generazione di thread, o pool di thread per fork per attività? (vedi questo riguardo al tuo problema specifico, per il pool di thread.) Anche questo breve documento delinea lo scopo e l'uso del modello del pool in modo conciso. Vedi queste risposte informative . Come notato qui, i pool di thread migliorano la scalabilità come bonus. Ed è "scrivi una volta, usa ovunque", invece di dover ottenere discussioni basate sul sottosistema per giocare bene ogni volta che scrivi un nuovo gioco o motore. Esistono anche molte solide soluzioni di pooling di thread di terze parti. Sarebbe più semplice iniziare con la generazione dei thread e lavorare in seguito verso i pool di thread, se è necessario mitigare l'overhead della generazione e della distruzione dei thread.


1
Qualche consiglio per librerie di pool di thread specifici da verificare?
imre

Nick, grazie mille per la risposta. Per quanto riguarda il tuo primo punto, penso che sia un'ottima idea e probabilmente la direzione in cui mi sposterò. Al momento è abbastanza presto che non so ancora cosa sarebbe necessario un doppio buffer. Lo terrò presente mentre si consolida nel tempo. Al tuo secondo punto, grazie per il suggerimento! Sì, i vantaggi delle attività di thread sono chiari. Leggerò i tuoi link e ci penserò. Non sono sicuro al 100% se funzionerà per me / come farlo funzionare per me, ma sicuramente ci penserò seriamente. Grazie!
Raptormeat

1
@imre controlla la libreria Boost - hanno dei futures, che sono un modo semplice e piacevole di avvicinarsi a queste cose.
Jonathan Dickinson,

5

Hai chiesto informazioni su diversi design multi-thread. Un mio amico mi ha parlato di questo metodo che pensavo fosse piuttosto interessante.

L'idea è che ci sarebbero 2 copie di ogni entità di gioco (dispendiosa, lo so). Una copia sarebbe la copia presente e l'altra sarebbe la copia passata. La presente copia è rigorosamente di sola scrittura e la copia precedente è di sola lettura . Quando vai all'aggiornamento, assegni intervalli del tuo elenco di entità a tutti i thread che ritieni più adatti. Ogni thread ha accesso in scrittura alle copie presenti nell'intervallo assegnato e ogni thread ha accesso in lettura a tutte le copie passate delle entità, quindi può aggiornare le copie presenti assegnate utilizzando i dati delle copie precedenti senza blocco. Tra ogni fotogramma, la copia presente diventa la copia passata, tuttavia si desidera gestire lo scambio di ruoli.


4

Abbiamo avuto lo stesso problema, solo con C #. Dopo aver riflettuto a lungo sulla facilità (o mancanza) di creare nuovi messaggi, il meglio che potevamo fare era creare un generatore di codice per loro. È un po 'brutto, ma utilizzabile: data solo una descrizione del contenuto del messaggio, genera classe di messaggio, enum, codice di gestione segnaposto ecc. - tutto questo codice che è quasi lo stesso ogni volta e veramente incline all'errore.

Non ne sono del tutto soddisfatto, ma è meglio che scrivere tutto quel codice a mano.

Per quanto riguarda i dati condivisi, la migliore risposta è ovviamente "dipende". Ma in genere, se alcuni dati vengono letti spesso e richiesti da molti thread, ne vale la pena condividerli. Per sicurezza del thread, la tua scommessa migliore è renderla immutabile , ma se è fuori discussione, mutex potrebbe fare. In C # c'è unReaderWriterLockSlim classe, progettata appositamente per tali casi; Sono sicuro che esiste un equivalente C ++.

Un'altra idea per la comunicazione di thread, che probabilmente risolve il tuo primo problema, è passare gestori invece di messaggi. Non sono sicuro di come risolverlo in C ++, ma in C # è possibile inviare un delegateoggetto a un altro thread (come in, aggiungerlo a un tipo di coda di messaggi) e effettivamente chiamare questo delegato dal thread di ricezione. Ciò rende possibile la creazione immediata di messaggi "ad hoc". Ho solo giocato con questa idea, non l'ho mai effettivamente provata in produzione, quindi potrebbe rivelarsi negativa in realtà.


Grazie per tutte le informazioni fantastiche! L'ultima parte sui gestori è simile a quella che stavo menzionando sull'uso di binding o funzioni per passare funzioni. Mi piace l'idea, potrei provarlo e vedere se fa schifo o è fantastico: D Potrebbe iniziare semplicemente creando una classe CallDelegateMessage e immergendo il dito del piede in acqua.
Raptormeat

1

Sono solo nella fase di progettazione di alcuni codici di gioco threaded, quindi posso solo condividere i miei pensieri, non nessuna esperienza reale. Detto questo, sto pensando secondo le seguenti linee:

  • La maggior parte dei dati di gioco deve essere condivisa per l'accesso in sola lettura .
  • La scrittura di dati è possibile utilizzando una sorta di messaggistica.
  • Per evitare di aggiornare i dati mentre un altro thread li sta leggendo, il loop del gioco ha due fasi distinte: lettura e aggiornamento.
  • Nella fase di lettura:
  • Tutti i dati condivisi sono di sola lettura per tutti i thread.
  • I thread possono calcolare cose (usando l'archiviazione locale dei thread) e produrre richieste di aggiornamento , che sono fondamentalmente oggetti comando / messaggio, inseriti in una coda, da applicare in seguito.
  • In fase di aggiornamento:
  • Tutti i dati condivisi sono di sola scrittura. I dati devono essere assunti in uno stato sconosciuto / instabile.
  • Qui vengono elaborati gli oggetti richiesta di aggiornamento.

Penso che (anche se non ne sono sicuro) in teoria ciò dovrebbe significare che durante le fasi di lettura e aggiornamento, qualsiasi numero di thread può essere eseguito contemporaneamente con una sincronizzazione minima. Nella fase di lettura nessuno sta scrivendo i dati condivisi, quindi non dovrebbero sorgere problemi di concorrenza. La fase di aggiornamento, beh, è ​​più complicata. Aggiornamenti paralleli sullo stesso pezzo di dati sarebbero un problema, quindi una sincronizzazione è in ordine qui. Tuttavia, potrei comunque eseguire un numero arbitrario di thread di aggiornamento, purché funzionino su set di dati diversi.

Nel complesso, penso che questo approccio si presterebbe bene a un sistema di pool di thread. Le parti problematiche sono:

  • Sincronizzazione dei thread di aggiornamento (assicurarsi che nessun thread multiplo tenti di aggiornare lo stesso set di dati).
  • Assicurarsi che nella fase di lettura nessun thread possa accidentalmente scrivere dati condivisi. Temo che ci sarebbe troppo spazio per gli errori di programmazione e non sono sicuro di quanti di essi possano essere facilmente individuati dagli strumenti di debug.
  • Scrivere il codice in modo tale da non poter fare affidamento sul fatto che i risultati intermedi siano immediatamente disponibili per la lettura. Cioè, non puoi scrivere x += 2; if (x > 5) ...se x è condiviso. È necessario creare una copia locale di x oppure produrre una richiesta di aggiornamento e eseguire solo il condizionale nell'esecuzione successiva. Quest'ultimo significherebbe un sacco di stato aggiuntivo thread-local che preserva il codice della piastra della caldaia.
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.