Mentre i thread possono accelerare l'esecuzione del codice, sono effettivamente necessari? Ogni pezzo di codice può essere fatto usando un singolo thread o esiste qualcosa che può essere realizzato solo usando più thread?
Mentre i thread possono accelerare l'esecuzione del codice, sono effettivamente necessari? Ogni pezzo di codice può essere fatto usando un singolo thread o esiste qualcosa che può essere realizzato solo usando più thread?
Risposte:
Prima di tutto, i thread non possono accelerare l'esecuzione del codice. Non rendono il computer più veloce. Tutto ciò che possono fare è aumentare l'efficienza del computer utilizzando il tempo che altrimenti verrebbe sprecato. In alcuni tipi di elaborazione questa ottimizzazione può aumentare l'efficienza e ridurre il tempo di esecuzione.
La semplice risposta è sì. Puoi scrivere qualsiasi codice da eseguire su un singolo thread. Prova: un sistema a singolo processore può eseguire le istruzioni in modo lineare. Avere più linee di esecuzione viene eseguito dagli interrupt di elaborazione del sistema operativo, salvando lo stato del thread corrente e avviandone un altro.
La risposta complessa è ... più complessa! La ragione per cui i programmi multithread possono essere spesso più efficienti di quelli lineari è a causa di un "problema" hardware. La CPU è in grado di eseguire calcoli più rapidamente rispetto alla memoria e all'IO della memoria effettiva. Pertanto, un'istruzione "add", ad esempio, viene eseguita molto più rapidamente di un "fetch". Le cache e il recupero delle istruzioni del programma dedicato (non sono sicuro del termine esatto qui) possono contrastare questo in una certa misura, ma il problema di velocità rimane.
Il threading è un modo per combattere questa discrepanza utilizzando la CPU per le istruzioni associate alla CPU mentre le istruzioni IO sono in fase di completamento. Un tipico piano di esecuzione del thread sarebbe probabilmente: recuperare dati, elaborare dati, scrivere dati. Supponiamo che il recupero e la scrittura richiedano 3 cicli e l'elaborazione richieda uno, a scopo illustrativo. Potete vedere che mentre il computer sta leggendo o scrivendo, non sta facendo nulla per 2 cicli ciascuno? Chiaramente è pigro e dobbiamo schioccare la nostra frusta di ottimizzazione!
Possiamo riscrivere il processo usando il threading per usare questo tempo sprecato:
E così via. Ovviamente questo è un esempio un po 'inventato, ma puoi vedere come questa tecnica può utilizzare il tempo che altrimenti verrebbe speso in attesa di IO.
Si noti che il threading come mostrato sopra può solo aumentare l'efficienza su processi fortemente legati all'IO. Se un programma calcola principalmente le cose, non ci saranno molti "buchi" in cui potremmo fare più lavoro. Inoltre, ci sono molte istruzioni per passare da un thread all'altro. Se si eseguono troppi thread, la CPU impiegherà la maggior parte del tempo a passare e non funziona molto sul problema. Questo si chiama thrashing .
Che tutto vada bene per un processore single core, ma la maggior parte dei processori moderni ha due o più core. I thread hanno ancora lo stesso scopo: massimizzare l'utilizzo della CPU, ma questa volta abbiamo la possibilità di eseguire due istruzioni separate contemporaneamente. Ciò può ridurre il tempo di esecuzione di un fattore per quanto siano disponibili molti core, poiché il computer è in realtà multitasking, non commutazione di contesto.
Con più core, i thread forniscono un metodo per dividere il lavoro tra i due core. Quanto sopra vale comunque per ogni singolo core; Un programma che esegue una massima efficienza con due thread su un core funzionerà molto probabilmente alla massima efficienza con circa quattro thread su due core. (L'efficienza è misurata qui con esecuzioni minime dell'istruzione NOP.)
I problemi con l'esecuzione di thread su più core (al contrario di un singolo core) sono generalmente risolti dall'hardware. La CPU sarà sicura di bloccare le posizioni di memoria appropriate prima di leggere / scrivere su di essa. (Ho letto che utilizza un bit di flag speciale in memoria per questo, ma questo potrebbe essere realizzato in diversi modi.) Come programmatore con linguaggi di livello superiore, non devi preoccuparti di nulla di più su due core come te avrebbe dovuto con uno.
TL; DR: i thread possono dividere il lavoro in modo da consentire al computer di elaborare diverse attività in modo asincrono. Ciò consente al computer di funzionare alla massima efficienza utilizzando tutto il tempo di elaborazione disponibile, anziché bloccarsi quando un processo è in attesa di una risorsa.
Cosa possono fare più thread che un singolo thread non può fare?
Niente.
Schizzo di prova semplice:
Si noti, tuttavia, che c'è un grande presupposto nascosto lì dentro: vale a dire che il linguaggio utilizzato all'interno del singolo thread è Turing-completo.
Quindi, la domanda più interessante potrebbe essere: "Può l'aggiunta di solo multi-threading per una lingua non-Turing-complete renderlo Turing-completo?" E credo che la risposta sia "Sì".
Prendiamo i linguaggi funzionali totali. [Per chi non ha familiarità: proprio come la programmazione funzionale è la programmazione con funzioni, la programmazione funzionale totale è la programmazione con funzioni totali.]
I linguaggi funzionali totali non sono ovviamente completi di Turing: non è possibile scrivere un ciclo infinito in un TFPL (in effetti, è praticamente la definizione di "totale"), ma è possibile in una macchina di Turing, ergo esiste almeno un programma che non può essere scritto in un TFPL ma può essere in un UTM, pertanto i TFPL sono meno potenti dal punto di vista computazionale degli UTM.
Tuttavia, non appena aggiungi il threading a un TFPL, ottieni infiniti loop: esegui ogni iterazione del loop in un nuovo thread. Ogni singolo thread restituisce sempre un risultato, quindi è Totale, ma ogni thread genera anche un nuovo thread che esegue la successiva iterazione, all'infinito.
Io penso che questa lingua sarebbe Turing-completo.
Per lo meno, risponde alla domanda originale:
Cosa possono fare più thread che un singolo thread non può fare?
Se si dispone di una lingua che non può fare un loop infinito, allora il multi-threading permette di fare cicli infiniti.
Nota, ovviamente, che la generazione di un thread è un effetto collaterale e quindi il nostro linguaggio esteso non è solo non più totale, non è nemmeno più funzionale.
In teoria, tutto ciò che fa un programma multithread può essere fatto anche con un programma a thread singolo, solo più lentamente.
In pratica, la differenza di velocità potrebbe essere così grande che non è possibile utilizzare un programma a thread singolo per l'attività. Ad esempio, se hai un processo di elaborazione dei dati batch in esecuzione ogni notte e ci vogliono più di 24 ore per terminare su un singolo thread, non hai altra opzione che renderlo multithread. (In pratica, la soglia è probabilmente anche inferiore: spesso tali attività di aggiornamento devono terminare entro la mattina presto, prima che gli utenti inizino a utilizzare nuovamente il sistema. Inoltre, altre attività possono dipendere da esse, che devono terminare anche nella stessa notte. l'autonomia disponibile può essere di poche ore / minuti.)
Fare lavoro di calcolo su più thread è una forma di elaborazione distribuita; stai distribuendo il lavoro su più thread. Un altro esempio di elaborazione distribuita (utilizzando più computer anziché più thread) è lo screensaver SETI: sgranocchiare che molti dati di misurazione su un singolo processore richiederebbe un tempo terribile e i ricercatori preferirebbero vedere i risultati prima del pensionamento ;-) Tuttavia, hanno non hanno il budget per affittare un supercomputer per così tanto tempo, quindi distribuiscono il lavoro su milioni di PC domestici, per renderlo economico.
Sebbene i thread sembrino essere un piccolo passo dal calcolo sequenziale, in realtà rappresentano un enorme passo. Scartano le proprietà più essenziali e attraenti del calcolo sequenziale: comprensibilità, prevedibilità e determinismo. I thread, come modello di calcolo, sono selvaggiamente non deterministici, e il lavoro del programmatore diventa quello di potare quel non determinismo.
- The Problem with Threads (www.eecs.berkeley.edu/Pubs/TechRpts/2006/EECS-2006-1.pdf).
Mentre ci sono alcuni vantaggi prestazionali che si possono ottenere usando i thread in quanto è possibile distribuire il lavoro su più core, spesso hanno un ottimo prezzo.
Uno degli svantaggi dell'utilizzo di thread non ancora menzionati qui è la perdita di compartimentazione delle risorse che si ottiene con spazi di processo a thread singolo. Ad esempio, supponiamo di imbattersi nel caso di un segfault. In alcuni casi è possibile riprendersi da questo in un'applicazione multi-processo in quanto si lascia semplicemente morire il bambino in errore e ne si respinge uno nuovo. Questo è il caso del backend prefork di Apache. Quando un'istanza httpd va a gonfie vele, il caso peggiore è che la particolare richiesta HTTP può essere ignorata per quel processo, ma Apache genera un nuovo figlio e spesso la richiesta se appena rinviata e revisionata. Il risultato finale è che Apache nel suo insieme non viene rimosso con il thread difettoso.
Un'altra considerazione in questo scenario sono le perdite di memoria. Ci sono alcuni casi in cui è possibile gestire con grazia un crash del thread (su UNIX, è possibile il recupero da alcuni segnali specifici - anche segfault / fpviolation -), ma anche in quel caso, è possibile che sia trapelata tutta la memoria allocata da quel thread (malloc, nuovo, ecc.). Quindi, mentre il processo può continuare a funzionare, perde sempre più memoria nel tempo ad ogni errore / ripristino. Ancora una volta, ci sono in qualche modo modi per minimizzare questo come l'uso di pool di memoria da parte di Apache. Ma ciò non protegge ancora dalla memoria che potrebbe essere stata allocata dalle librerie di terze parti che il thread potrebbe aver utilizzato.
E, come alcune persone hanno sottolineato, comprendere le primitive di sincronizzazione è forse la cosa più difficile da ottenere davvero. Questo problema di per sé - solo ottenere la logica generale giusta per tutto il codice - può essere un grosso mal di testa. È possibile che si verifichino deadlock misteriosi nei momenti più strani, e talvolta nemmeno fino a quando il programma non è stato avviato in produzione, il che rende il debug ancora più difficile. Aggiungete a ciò il fatto che le primitive di sincronizzazione variano spesso ampiamente con la piattaforma (Windows vs. POSIX) e il debug può essere spesso più difficile, nonché la possibilità di condizioni di gara in qualsiasi momento (avvio / inizializzazione, runtime e spegnimento), la programmazione con thread ha davvero poca pietà per i principianti. E anche per esperti, c'è ancora poca misericordia solo perché la conoscenza del threading stesso non minimizza la complessità in generale. Ogni riga di codice threaded sembra talvolta aggravare in modo esponenziale la complessità complessiva del programma e aumentare la probabilità che emergano in qualsiasi momento un deadlock nascosto o una strana condizione di razza. Può anche essere molto difficile scrivere casi di prova per scovare queste cose.
Questo è il motivo per cui alcuni progetti come Apache e PostgreSQL sono per lo più basati su processi. PostgreSQL esegue ogni thread back-end in un processo separato. Naturalmente questo non allevia ancora il problema della sincronizzazione e delle condizioni di gara, ma aggiunge un bel po 'di protezione e in qualche modo semplifica le cose.
Più processi ciascuno che esegue un singolo thread di esecuzione può essere molto meglio di più thread in esecuzione in un singolo processo. E con l'avvento di gran parte del nuovo codice peer-to-peer come AMQP (RabbitMQ, Qpid, ecc.) E ZeroMQ, è molto più semplice dividere i thread tra diversi spazi di processo e persino macchine e reti, semplificando notevolmente le cose. Ma comunque, non è un proiettile d'argento. C'è ancora complessità da affrontare. Basta spostare alcune delle variabili dallo spazio del processo alla rete.
La linea di fondo è che la decisione di entrare nel dominio dei thread non è leggera. Una volta che ti avventuri in quel territorio, quasi istantaneamente tutto diventa più complesso e intere nuove razze di problemi entrano nella tua vita. Può essere divertente e bello, ma è come l'energia nucleare - quando le cose vanno male, possono andare male e velocemente. Ricordo di aver seguito un corso di addestramento sulla criticità molti anni fa e hanno mostrato le foto di alcuni scienziati di Los Alamos che hanno suonato con il plutonio nei laboratori della seconda guerra mondiale. Molti hanno preso poche o nessuna precauzione contro l'evento di un'esposizione e in un batter d'occhio - in un singolo lampo luminoso e indolore, tutto sarebbe finito per loro. Giorni dopo erano morti. Richard Feynman in seguito si riferì a questo come " solleticare la coda del drago"È un po 'come giocare ai thread (almeno per me comunque). All'inizio sembra piuttosto innocuo, e quando ti mordi, ti gratti la testa con la velocità con cui le cose sono andate male. Ma almeno i thread hanno vinto ti uccido.
Innanzitutto, un'applicazione a thread singolo non trarrà mai vantaggio da una CPU multi-core o da hyper-threading. Ma anche su un singolo core, la CPU a thread singolo che esegue il multi-threading presenta vantaggi.
Considera l'alternativa e se ciò ti rende felice. Supponiamo di avere più attività che devono essere eseguite contemporaneamente. Ad esempio, devi continuare a comunicare con due diversi sistemi. Come si fa senza multi-threading? Probabilmente dovresti creare il tuo programmatore e lasciarlo chiamare le diverse attività che devono essere eseguite. Ciò significa che è necessario suddividere le attività in parti. Probabilmente è necessario soddisfare alcuni vincoli in tempo reale e assicurarsi che le parti non impieghino troppo tempo. Altrimenti il timer scadrà in altre attività. Questo rende più difficile dividere un'attività. Più attività devi gestire, più suddivisione devi svolgere e più complessa sarà la tua pianificazione per soddisfare tutti i vincoli.
Quando hai più thread, la vita può diventare più semplice. Uno scheduler preventivo può interrompere un thread in qualsiasi momento, mantenerne lo stato e riavviarne un altro. Si riavvierà quando il tuo thread inizia il suo turno. Vantaggi: la complessità di scrivere uno scheduler è già stata fatta per te e non devi dividere i tuoi compiti. Inoltre, lo scheduler è in grado di gestire processi / thread di cui non sei nemmeno a conoscenza. Inoltre, quando un thread non ha bisogno di fare nulla (è in attesa di un evento) non richiede cicli di CPU. Questo non è così facile da realizzare quando si crea lo scheduler down a thread singolo. (mettere a dormire qualcosa non è così difficile, ma come si sveglia?)
Il rovescio della medaglia dello sviluppo multi-thread è che è necessario conoscere i problemi di concorrenza, le strategie di blocco e così via. Lo sviluppo di codice multi-thread senza errori può essere piuttosto difficile. E il debug può essere ancora più difficile.
esiste qualcosa che può essere realizzato solo usando più thread?
Sì. Non è possibile eseguire codice su più CPU o core della CPU con un singolo thread.
Senza più CPU / core, i thread possono ancora semplificare il codice che viene eseguito concettualmente in parallelo, come la gestione dei client su un server, ma è possibile fare la stessa cosa senza thread.
Le discussioni non riguardano solo la velocità ma anche la concorrenza.
Se non hai un'applicazione batch come suggerito da @Peter ma invece un toolkit GUI come WPF come puoi interagire con gli utenti e la logica aziendale con un solo thread?
Supponiamo inoltre di creare un server Web. Come servirebbe più di un utente contemporaneamente con un solo thread (supponendo che nessun altro processo)?
Esistono molti scenari in cui un solo thread semplice non è sufficiente. Ecco perché sono in corso recenti progressi come il processore Intel MIC con oltre 50+ core e centinaia di thread.
Sì, la programmazione parallela e simultanea è difficile. Ma necessario.
Il multi-threading può rendere l'interfaccia della GUI ancora reattiva durante le lunghe operazioni di elaborazione. Senza il multi-threading, l'utente sarebbe bloccato a guardare un modulo bloccato mentre è in esecuzione un processo lungo.
Il codice multi-thread può bloccare la logica del programma e accedere ai dati non aggiornati in modi impossibili per i singoli thread.
I thread possono prendere un bug oscuro da qualcosa che un programmatore medio può aspettarsi di eseguire il debug e spostarlo nel regno in cui vengono raccontate storie della fortuna necessaria per catturare lo stesso bug con i pantaloni quando un programmatore di allarme stava guardando solo il momento giusto.
le app che si occupano del blocco degli I / O che devono anche rimanere sensibili ad altri input (la GUI o altre connessioni) non possono essere rese singlethreaded
l'aggiunta di metodi di controllo nella libreria IO per vedere quanto può essere letto senza bloccare può aiutare questo, ma non molte librerie offrono garanzie complete a riguardo
Molte buone risposte, ma non sono sicuro che una frase sia esattamente come farei - Forse questo offre un modo diverso di vederlo:
I thread sono solo una semplificazione della programmazione come Oggetti o Attori o per i loop (Sì, tutto ciò che si implementa con i loop che è possibile implementare con if / goto).
Senza thread si implementa semplicemente un motore di stato. Ho dovuto farlo molte volte (la prima volta che l'ho fatto non ne avevo mai sentito parlare - ho appena fatto una grande dichiarazione switch controllata da una variabile "State"). Le macchine a stati sono ancora abbastanza comuni ma possono essere fastidiose. Con i fili, un enorme pezzo della piastra della caldaia scompare.
Capita anche che sia più facile per un linguaggio interrompere la sua esecuzione in runtime in blocchi compatibili con più CPU (anche gli attori, credo).
Java fornisce thread "verdi" su sistemi in cui il sistema operativo non fornisce QUALSIASI supporto di threading. In questo caso è più facile vedere che chiaramente non sono altro che un'astrazione di programmazione.
Il sistema operativo utilizza il concetto di suddivisione del tempo in cui ogni thread ottiene il tempo di essere eseguito e quindi viene annullato. Un approccio del genere può sostituire il threading così com'è ora, ma scrivere i propri programmatori in ogni applicazione sarebbe eccessivo. Inoltre, dovresti lavorare con i dispositivi I / O e così via. E richiederebbe un po 'di supporto dal lato hardware, in modo da poter attivare gli interrupt per far funzionare lo scheduler. Fondamentalmente dovresti scrivere un nuovo sistema operativo ogni volta.
In generale, il threading può migliorare le prestazioni nei casi in cui i thread attendono l'I / O o sono inattivi. Consente inoltre di creare interfacce reattive e di interrompere i processi mentre si eseguono attività lunghe. Inoltre, il threading migliora le cose su CPU multicore reali.
Innanzitutto, i thread possono fare due o più cose contemporaneamente (se hai più di un core). Sebbene sia possibile farlo anche con più processi, alcune attività non si distribuiscono molto bene su più processi.
Inoltre, in alcune attività sono presenti spazi che non è possibile evitare facilmente. Ad esempio, è difficile leggere i dati da un file su disco e fare in modo che il processo faccia qualcos'altro allo stesso tempo. Se l'attività richiede necessariamente molti dati di lettura dal disco, il processo impiegherà molto tempo ad aspettare il disco, qualunque cosa tu faccia.
In secondo luogo, i thread possono consentirti di evitare di dover ottimizzare grandi quantità di codice che non è critico per le prestazioni. Se hai un solo thread, ogni pezzo di codice è critico per le prestazioni. Se si blocca, sei affondato: nessuna attività che verrebbe eseguita da quel processo può fare progressi. Con i thread, un blocco influisce solo sul fatto che il thread e altri thread possono venire avanti e lavorare su attività che devono essere eseguite da quel processo.
Un buon esempio è il codice di gestione degli errori eseguito di rado. Supponiamo che un'attività rilevi un errore molto raro e che il codice per gestirlo debba scorrere nella memoria. Se il disco è occupato e il processo ha solo un singolo thread, non è possibile eseguire progressi in avanti fino a quando il codice per gestire quell'errore non può essere caricato in memoria. Ciò può causare una risposta affrettata.
Un altro esempio è se molto raramente è necessario eseguire una ricerca nel database. Se aspetti che il database risponda, il tuo codice colpirà un enorme ritardo. Ma non vuoi preoccuparti di rendere tutto questo codice asincrono perché è così raro che devi fare queste ricerche. Con un thread per fare questo lavoro, ottieni il meglio da entrambi i mondi. Un thread per fare questo lavoro lo rende non critico come dovrebbe essere.