Qual è la differenza tra un coroutine e un thread? Ci sono dei vantaggi nell'usare l'uno rispetto all'altro?
Qual è la differenza tra un coroutine e un thread? Ci sono dei vantaggi nell'usare l'uno rispetto all'altro?
Risposte:
Mentre le Coroutine sembrano funzionare come thread a prima vista, in realtà non usano alcun multithreading. Vengono eseguiti in sequenza fino a quando yield
. Il motore controllerà tutte le coroutine rese come parte del proprio loop principale (in quale punto dipende esattamente dal tipo di yield
, controlla questo diagramma per ulteriori informazioni ), continuandole una dopo l'altra fino alla successiva yield
, quindi procederà con il loop principale.
Questa tecnica ha il vantaggio di poter utilizzare le coroutine senza i mal di testa causati da problemi con il multithreading reale. Non otterrai deadlock, condizioni di gara o problemi di prestazioni causati da switch di contesto, sarai in grado di eseguire il debug in modo corretto e non dovrai utilizzare contenitori di dati thread-safe. Questo perché durante l'esecuzione di una routine, il motore Unity si trova in uno stato controllato. È sicuro utilizzare la maggior parte delle funzionalità di Unity.
Con i thread, d'altra parte, non hai assolutamente alcuna conoscenza dello stato in cui si trova attualmente il ciclo principale di Unity (potrebbe in effetti non funzionare più). Quindi il tuo thread potrebbe causare un sacco di caos facendo qualcosa alla volta che non dovrebbe fare quella cosa. Non toccare alcuna funzionalità nativa di Unity da un sottoprocesso . Se è necessario comunicare tra un sottoprocesso e il thread principale, fare in modo che il thread scriva su un oggetto contenitore thread-safe (!) E si faccia leggere a MonoBehaviour tali informazioni durante le normali funzioni dell'evento Unity.
Lo svantaggio di non eseguire il multithreading "reale" è che non è possibile utilizzare le coroutine per parallelizzare i calcoli intensi della CPU su più core della CPU. Puoi usarli, tuttavia, per dividere un calcolo su più aggiornamenti. Quindi, invece di congelare il gioco per un secondo, ottieni solo un framerate medio più basso per più secondi. Ma in tal caso sei responsabile della yield
tua routine ogni volta che vuoi consentire a Unity di eseguire un aggiornamento.
Conclusione:
Le coroutine sono ciò che in Informatica chiamiamo "multitasking cooperativo". Sono un modo per più flussi diversi di esecuzione interagire tra loro in modo cooperativo. Nel multitasking cooperativo un flusso di esecuzione ha la sola proprietà indiscussa della CPU fino a quando non raggiunge un yield
. A quel punto, Unity (o qualunque framework tu stia usando), ha la possibilità di passare a un diverso flusso di esecuzione. Quindi ottiene anche la sola proprietà indiscussa della CPU fino a quando non lo yield
è.
Le discussioni sono ciò che chiamiamo "multitasking preventivo". Quando si utilizzano i thread, il framework si riserva il diritto in qualsiasi momento di interrompere la discussione a metà e passare a un altro thread. Non importa dove ti trovi. In alcuni casi puoi persino essere interrotto durante la scrittura di una variabile in memoria!
Ci sono pro e contro per ciascuno. I contro delle coroutine sono probabilmente i più facili da capire. Prima di tutto, le coroutine vengono eseguite su un singolo core. Se si dispone di una CPU quad core, le coroutine utilizzeranno solo uno dei quattro core. Questo semplifica le cose, ma in alcuni casi può essere un problema di prestazioni. Il secondo svantaggio è che devi essere consapevole del fatto che qualsiasi procedura di coroutine può fermare l'intero programma semplicemente rifiutando di farlo yield
. Questo è stato un problema su Mac OS9, molti anni fa. OS9 supportava solo il multitasking cooperativo, nell'intero computer. Se uno dei tuoi programmi si bloccasse, potrebbe arrestare il computer in modo così violento che il sistema operativo non potrebbe nemmeno visualizzare il testo del messaggio di errore per farti sapere cosa è successo!
I pro delle coroutine sono che sono relativamente facili da capire. Gli errori che hai sono molto più prevedibili. In genere richiedono anche meno risorse, il che può essere utile mentre sali nelle decine di migliaia di coroutine o fili. Candid Moon ha menzionato nei commenti che, se non hai studiato correttamente le discussioni, resta fedele alle coroutine e hanno ragione. Le coroutine sono molto più semplici con cui lavorare.
Le discussioni sono completamente una bestia diversa. Devi sempre stare in guardia contro la possibilità che un altro thread possa interromperti in qualsiasi momentoe pasticciare con i tuoi dati. Le librerie di threading forniscono intere suite di potenti strumenti per aiutarti in questo, come mutex e variabili di condizione che ti aiutano a dire al sistema operativo quando è sicuro che esegua uno degli altri thread e quando non è sicuro. Ci sono interi corsi dedicati a come usare bene questi strumenti. Uno dei problemi famosi che si presentano è un "deadlock", ovvero quando due thread si "bloccano" entrambi aspettando che l'altro liberi alcune risorse. Un altro problema, che è molto importante per Unity, è che molte librerie (come Unity) non sono progettate per supportare chiamate da più thread. È possibile interrompere facilmente il framework se non si presta attenzione a quali chiamate sono consentite e quali sono vietate.
Il motivo di questa ulteriore complessità è in realtà molto semplice. Il modello di multitasking preventivo è davvero simile al modello di multithreading che consente non solo di interrompere altri thread, ma di eseguire effettivamente thread affiancati su core diversi. Questo è incredibilmente potente, essendo l'unico modo per sfruttare davvero queste nuove CPU quad core ed esadecimali che stanno uscendo, ma apre la finestra di Pandora. Le regole di sincronizzazione su come gestire questi dati in un mondo multithreading sono decisamente brutali. Nel mondo C ++, ci sono interi articoli dedicati ai MEMORY_ORDER_CONSUME
quali è un angolo di sincronizzazione multithreading.
Quindi i contro del threading? Semplice: sono difficili. Puoi imbatterti in intere classi di bug che non hai mai visto prima. Molti sono i cosiddetti "heisenbugs" che a volte compaiono e poi scompaiono quando li esegui il debug. Gli strumenti che ti vengono dati per gestirli sono molto potenti, ma sono anche di livello molto basso. Sono progettati per essere efficienti sulle architetture dei chip moderni anziché essere progettati per essere facili da usare.
Tuttavia, se si desidera utilizzare tutta la potenza della CPU, sono lo strumento necessario. Inoltre, ci sono in realtà algoritmi che sono più facili da capire nel multithreading rispetto a quelli con le coroutine semplicemente perché si lascia che il sistema operativo gestisca tutte le domande su dove possono verificarsi interruzioni.
Anche il commento di Candid Moon per attenersi alle coroutine è la mia raccomandazione. Se vuoi il potere dei thread, allora impegnati. Esci e impara davvero le discussioni, formalmente. Abbiamo avuto diversi decenni per capire come organizzare il modo migliore di pensare ai thread in modo da ottenere risultati affidabili ripetibili e affidabili in anticipo e aggiungere prestazioni mentre procedi. Ad esempio, tutti i corsi sani insegneranno i mutex prima di insegnare le variabili di condizione. Tutti i corsi sani che coprono l'atomica insegneranno completamente i mutex e condizioneranno le variabili prima ancora di menzionare che esiste l'atomica. (Nota: non esiste un tutorial sensato sull'atomica.) Cerca di imparare il threading frammentario e stai chiedendo emicrania.
join()
, ma hai bisogno di qualcosa. Se non hai un architetto che ha progettato un sistema per eseguire la sincronizzazione per te, devi scriverlo tu stesso. Come qualcuno che fa cose multithread, trovo che i modelli mentali delle persone su come i computer lavorano devono essere adattati prima di eseguire la sincronizzazione in modo sicuro (che è ciò che insegneranno i buoni corsi)
join
. Il mio punto era che perseguire una frazione moderata dei possibili vantaggi in termini di prestazioni del threading, a volte, potrebbe talvolta essere meglio che utilizzare un approccio di threading più complicato per ottenere una frazione maggiore.
Nei termini più semplici possibili ...
discussioni
Un thread non decide quando cede, il sistema operativo (il "sistema operativo", ad esempio Windows) decide quando viene restituito un thread. Il sistema operativo è quasi interamente responsabile della pianificazione dei thread, decide quali thread eseguire, quando eseguirli e per quanto tempo.
Inoltre, un thread potrebbe essere eseguito in modo sincrono (un thread dopo l'altro) o in modo asincrono (thread diversi in esecuzione su diversi core della CPU). La possibilità di eseguire in modo asincrono significa che i thread possono svolgere più lavoro nello stesso lasso di tempo (poiché i thread stanno letteralmente facendo due cose contemporaneamente). Anche i thread sincroni svolgono molto lavoro se il sistema operativo è in grado di programmarli.
Tuttavia, questa potenza di elaborazione aggiuntiva comporta effetti collaterali. Ad esempio, se due thread stanno tentando di accedere alla stessa risorsa (ad esempio un elenco) e ogni thread può essere arrestato in modo casuale in qualsiasi punto del codice, le modifiche del secondo thread potrebbero interferire con le modifiche apportate dal primo thread. (Vedi anche: Condizioni di gara e deadlock .)
I thread sono anche considerati "pesanti" perché hanno un sacco di sovraccarico, il che significa che si verifica una notevole penalità di tempo quando si cambia thread.
coroutine
A differenza dei thread, le coroutine sono completamente sincrone, è possibile eseguire solo una coroutine in qualsiasi momento. Inoltre, le coroutine possono scegliere quando cedere, e quindi possono scegliere di cedere in un punto del codice conveniente (ad es. Alla fine di un ciclo). Questo ha il vantaggio di problemi come le condizioni di gara e gli deadlock molto più facili da evitare, oltre a facilitare la cooperazione reciproca tra le coroutine.
Tuttavia, questa è anche una delle maggiori responsabilità, se un coroutine non cede correttamente potrebbe finire per consumare molto tempo del processore e può comunque causare bug se modifica in modo errato le risorse condivise.
Le coroutine generalmente non richiedono alcun cambio di contesto e sono quindi veloci da accendere e spegnere e sono abbastanza leggere.
In sintesi:
Filo:
coroutine:
I ruoli di thread e coroutine sono molto simili, ma differiscono nel modo in cui svolgono il lavoro, il che significa che ognuno è più adatto a compiti diversi. I thread sono i migliori per le attività in cui possono concentrarsi sul fare qualcosa da soli senza essere interrotti, quindi segnalando quando hanno finito. Le coroutine sono le migliori per le attività che possono essere eseguite in molti piccoli passaggi e attività che richiedono l'elaborazione cooperativa dei dati.