Come devo strutturare le mie classi per consentire la simulazione multithread?


8

Nel mio gioco, ci sono appezzamenti di terra con edifici (case, centri di risorse). Gli edifici come case hanno inquilini, stanze, componenti aggiuntivi, eccetera, e ci sono diversi valori che devono essere simulati sulla base di tutte queste variabili.

Ora, vorrei usare AndEngine per le cose front-end e creare un altro thread per eseguire i calcoli di simulazione (forse anche in seguito includere l'IA in questo thread). Questo è così che un intero thread non fa tutto il lavoro e causa problemi come il blocco. Questo introduce il problema della concorrenza e della dipendenza .

Il problema di valuta è il mio thread principale dell'interfaccia utente e il thread di calcolo dovrebbe entrambi accedere a tutti gli oggetti di simulazione. Quindi devo renderli sicuri per i thread, ma non so come archiviare e strutturare gli oggetti di simulazione per abilitarlo.

Il problema della dipendenza è che per calcolare i valori, i miei calcoli dipendono dai valori di altri oggetti.

Quale sarebbe il modo migliore per collegare il mio oggetto inquilino nell'edificio con i miei calcoli? Codificarlo nella classe dei tenant? Qual è un buon modo per "memorizzare" gli algoritmi in modo che possano essere facilmente modificati?

Un modo semplice e pigro sarebbe di mettere tutto in una classe che contiene tutti gli oggetti, come trame di terra (che a loro volta tengono gli edifici, eccetera). Questa classe manterrebbe anche lo stato di gioco come la tecnologia disponibile per l'utente, i pool di oggetti per cose come gli sprite. Ma questo è un modo pigro e pericoloso, giusto?

Modifica: stavo guardando Dependency Injection, ma quanto riesce a farcela come una classe che contiene altri oggetti? cioè, il mio appezzamento di terra, con un edificio, che ha un inquilino e una miriade di altri valori. DI sembra un dolore nel culo anche con AndEngine.


Solo una breve nota, non vi è alcuna preoccupazione sull'accesso simultaneo ai dati se uno degli accessi è sempre e solo di sola lettura. Finché si mantiene il rendering solo leggendo i dati non elaborati per utilizzarlo per il rendering e non aggiornare i dati durante l'elaborazione, non vi è alcun problema. Un thread aggiorna i dati, l'altro thread li legge e li rende.
James,

Bene, l'accesso alla concorrenza è ancora un problema, poiché l'utente può acquistare un appezzamento di terra, costruire un edificio su quella terra e mettere un inquilino in una casa, quindi il thread principale sta creando dati e può modificarli. L'accesso simultaneo non è un problema, riguarda piuttosto la sua istanza di condivisione tra il thread principale e il thread figlio.
NiffyShibby,

Parlo della dipendenza come un problema, a quanto pare gente come Google pensa che nascondere la dipendenza non sia una cosa saggia. I miei calcoli degli inquilini dipendono dal terreno edificabile, dalla costruzione, dalla creazione di uno sprite sullo schermo (potrei avere una relazione relazionale tra un inquilino dell'edificio e dalla creazione di uno sprite dell'inquilino altrove)
NiffyShibby,

Immagino che il mio suggerimento debba essere interpretato come fare in modo che le cose che vengono disconnesse siano cose autosufficienti o che richiedano l'accesso in sola lettura ai dati gestiti da un altro thread. Il rendering sarebbe un esempio di qualcosa che potresti scartare come sarebbe necessita solo dell'accesso in lettura ai dati per poterli visualizzare.
James,

1
James, anche un accesso di sola lettura può essere una cattiva idea se un altro thread è nel mezzo di apportare modifiche a quell'oggetto. Con una struttura di dati complessa potrebbe causare un arresto anomalo e con tipi di dati semplici potrebbe causare una lettura incoerente.
Kylotan,

Risposte:


4

Il problema è intrinsecamente seriale: è necessario completare un aggiornamento della simulazione prima di poterlo rendere. Scaricare la simulazione su un thread diverso significa semplicemente che il thread dell'interfaccia utente principale non fa nulla mentre il thread di simulazione scorre (il che significa che è bloccato).

La "best practice" comunemente utilizzata per la concorrenza non è quella di mettere il rendering su un thread e la simulazione su un altro, come si propone. Consiglio vivamente contro questo approccio, in effetti. Le due operazioni sono naturalmente correlate in serie e, sebbene possano essere forzate brutalmente, non è ottimale e non si ridimensiona .

Un approccio migliore consiste nel rendere simultanee parti dell'aggiornamento o del rendering, ma lasciare l'aggiornamento e il rendering stessi sempre seriali. Quindi, ad esempio, se hai un limite naturale nella tua simulazione (ad esempio, se le case non si influenzano mai a vicenda nella tua simulazione) puoi spingere tutte le case in secchi di N case e far girare un mucchio di fili che ognuno elabora uno bucket e lascia che quei thread si uniscano prima che il passaggio di aggiornamento sia completo. Questo si adatta molto meglio e si adatta molto meglio alla progettazione concorrente.

Stai pensando troppo al resto del problema:

L'iniezione di dipendenza è un'aringa rossa qui: tutta l'iniezione di dipendenza significa davvero che si passa ("inject") le dipendenze di un'interfaccia alle istanze di quell'interfaccia, in genere durante la costruzione.

Ciò significa che se hai una classe che modella un a House, che ha bisogno di sapere cose in Citycui si trova, il Housecostruttore potrebbe apparire come:

public House( City containingCity ) {
  m_city = containingCity; // Store in a member variable for later access
  ...
}

Niente di speciale.

L'uso di un singleton non è necessario (lo si vede spesso in alcuni dei "DI framework" follemente complessi e sovraingegnerizzati come Caliburn progettati per applicazioni GUI "enterprise" - questo non lo rende una buona soluzione). In effetti, l'introduzione di singleton è spesso l'antitesi di una buona gestione delle dipendenze. Possono anche causare seri problemi con il codice multithread perché di solito non possono essere resi thread-safe senza blocchi: più blocchi si devono acquisire, peggio è il problema adatto alla gestione in una natura parallela.


Ricordo di aver detto che i singoli erano cattivi nel mio post originale ...
NiffyShibby,

Ricordo di aver detto che i singoli erano cattivi nel mio post originale, ma che è stato rimosso. Penso di ottenere quello che stai dicendo. Ad esempio, la mia piccola persona sta camminando attraverso uno schermo, mentre sta facendo chiamare il thread di aggiornamento, deve aggiornarlo, ma non può perché il thread principale sta usando l'oggetto, quindi l'altro mio thread è bloccato. Dove come dovrei aggiornare tra il rendering.
NiffyShibby,

Qualcuno mi ha inviato un link utile. gamedev.stackexchange.com/questions/95/…
NiffyShibby

5

La solita soluzione per problemi di concorrenza è l' isolamento dei dati .

Isolamento significa che ogni thread ha i propri dati e non tocca i dati di altri thread. In questo modo non ci sono problemi con la concorrenza ... ma poi abbiamo un problema di comunicazione. Come possono lavorare insieme questi thread se non condividono dati?

Ci sono due approcci qui.

Il primo è l' immutabilità . Strutture / variabili immutabili sono quelle che non cambiano mai il loro stato. All'inizio, questo può sembrare inutile: come si può usare una "variabile" che non cambia mai? Tuttavia, possiamo scambiare queste variabili! Considera questo esempio: supponi di avere una Tenantclasse con un mucchio di campi, che deve essere in uno stato coerente. Se si modifica un Tenantoggetto nel thread A e allo stesso tempo lo si osserva dal thread B, il thread B potrebbe vedere l'oggetto in uno stato incoerente. Tuttavia, se Tenantè immutabile, il thread A non può cambiarlo. Invece, crea nuovo Tenantoggetto con i campi impostati come richiesto e lo scambia con quello vecchio. Lo scambio è solo una modifica a un riferimento, che è probabilmente atomico, e quindi non c'è modo di osservare l'oggetto in uno stato incoerente.

Il secondo approccio è la messaggistica . L'idea alla base è che quando tutti i dati sono "posseduti" da qualche thread, possiamo dire a questo thread cosa fare con i dati. Ogni thread in questa architettura ha una coda di messaggi - un elenco di Messageoggetti e una pompa di messaggistica - che esegue costantemente un metodo che rimuove un messaggio dalla coda, lo interpreta e chiama un metodo gestore. Ad esempio, supponiamo di aver attinto a un appezzamento di terreno, segnalando che deve essere acquistato. Il thread dell'interfaccia utente non può modificare Plotdirettamente l' oggetto, poiché appartiene al thread logico (e probabilmente è immutabile). Quindi il thread dell'interfaccia utente costruisce BuyMessageinvece un oggetto e lo aggiunge alla coda del thread logico. Il thread logico, durante l'esecuzione, prende il messaggio dalla coda e chiamaBuyPlot(), estraendo i parametri dall'oggetto messaggio. Potrebbe inviare un messaggio indietro, ad esempio BuySuccessfulMessage, chiedendo al thread dell'interfaccia utente di inserire un "Ora hai più terra!" finestra sullo schermo. Naturalmente, l'accesso alla coda dei messaggi deve essere sincronizzato con il blocco, la sezione critica o qualunque cosa venga chiamata in AndEngine. Ma questo è un singolo punto di sincronizzazione tra i thread e i thread vengono sospesi per un tempo molto breve, quindi non è un problema.

Questi due approcci sono meglio usati in combinazione. I tuoi thread dovrebbero comunicare con i messaggi e avere alcuni dati immutabili "aperti" per altri thread, ad esempio un elenco immutabile di grafici per l'interfaccia utente per disegnarli.

Si noti, inoltre, che "sola lettura" fa non necessariamente significa immutabile ! Qualsiasi struttura di dati complessa come una tabella hash può cambiare il suo stato interno agli accessi in lettura, quindi controlla prima con la documentazione.


Sembra un modo pulito, dovrò fare qualche test con esso, sembra abbastanza costoso in questo modo, stavo pensando lungo le linee di DI con un ambito di singleton, quindi uso i blocchi per l'accesso simultaneo. Ma non ho mai pensato di farlo in questo modo, potrebbe funzionare: D
NiffyShibby,

Bene, è così che facciamo concorrenza su server multithread concomitanti. Probabilmente un po 'eccessivo per un gioco semplice, ma questo è l'approccio che userei da solo.
Nevermind

4

Probabilmente il 99% dei programmi per computer scritti nella storia utilizzava solo 1 thread e funzionava bene. Non ho alcuna esperienza di AndEngine ma è molto raro trovare sistemi che richiedono il threading, solo alcuni che potrebbero averne beneficiato, dato l'hardware giusto.

Tradizionalmente, per eseguire la simulazione e la GUI / rendering in un thread, fai semplicemente un po 'di simulazione, quindi esegui il rendering e ripeti, in genere molte volte al secondo.

Quando qualcuno ha poca esperienza nell'uso di più processi o non apprezza pienamente il significato di "sicurezza" del thread (che è un termine vago che può significare molte cose diverse), è troppo facile introdurre molti bug in un sistema. Quindi personalmente consiglierei di adottare l'approccio a thread singolo, la simulazione e il rendering interleaving e di salvare qualsiasi threading per operazioni che si sa che impiegheranno molto tempo e che richiedono assolutamente thread e non un modello basato su eventi.


Andengine fa il rendering per me, ma sento ancora che i calcoli devono andare in un altro thread, poiché il thread principale dell'interfaccia utente finirebbe per rallentare se non bloccato se tutto fosse fatto in un thread.
NiffyShibby,

Perché lo senti? Hai calcoli che sono più costosi di un tipico gioco 3D? E sei consapevole del fatto che la maggior parte dei dispositivi Android ha solo 1 core e quindi non ottiene alcun vantaggio intrinseco in termini di prestazioni da thread aggiuntivi?
Kylotan,

No, è bello separare la logica e definire chiaramente ciò che viene fatto, se lo tieni nello stesso thread, dovresti fare riferimento alla classe principale in cui si trova o fare un DI con ambito singleton. Il che non è un problema. Per quanto riguarda il core, stiamo vedendo emergere più dispositivi Android dual core, la mia idea di gioco potrebbe non funzionare affatto bene su un dispositivo single core mentre su un dual core potrebbe funzionare abbastanza bene.
NiffyShibby,

Quindi progettare tutto in 1 thread intero non mi sembra un'ottima idea, almeno con i thread posso separare la logica e in futuro non dovrò preoccuparmi di provare a migliorare le prestazioni come ho progettato dall'inizio.
NiffyShibby,

Ma il tuo problema è ancora intrinsecamente seriale, quindi probabilmente bloccherai entrambi i thread in attesa che si uniscano comunque, a meno che tu non abbia isolato i dati (dando al thread di rendering qualcosa da fare effettivamente mentre il thread logico ticchetta) e rendi un frame circa dietro la simulazione. L'approccio che stai descrivendo non è la best practice comunemente accettata per la progettazione di concorrenza.
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.