Perché il multithreading è spesso preferito per migliorare le prestazioni?


23

Ho una domanda, riguarda il motivo per cui i programmatori sembrano amare la concorrenza e i programmi multi-thread in generale.

Sto prendendo in considerazione 2 approcci principali qui:

  • un approccio asincrono fondamentalmente basato su segnali, o semplicemente un approccio asincrono come chiamato da molti documenti e linguaggi come ad esempio il nuovo C # 5.0 e un "thread associato" che gestisce la politica della tua pipeline
  • un approccio simultaneo o multi-threading

Dirò solo che sto pensando all'hardware qui e allo scenario peggiore, e ho testato personalmente questi 2 paradigmi, il paradigma asincrono è un vincitore al punto che non capisco perché la gente il 90% delle volte parlare di multi-threading quando vogliono velocizzare le cose o fare un buon uso delle loro risorse.

Ho testato programmi multi-thread e programma asincrono su una vecchia macchina con un quad-core Intel che non offre un controller di memoria all'interno della CPU, la memoria è gestita interamente dalla scheda madre, bene in questo caso le prestazioni sono orribili con un un'applicazione multi-thread, anche un numero relativamente basso di thread come 3-4-5 può essere un problema, l'applicazione non risponde ed è solo lenta e spiacevole.

Un buon approccio asincrono è, d'altra parte, probabilmente non più veloce, ma non è neanche peggio, la mia applicazione attende solo il risultato e non si blocca, è reattiva e c'è un ridimensionamento molto migliore in corso.

Ho anche scoperto che un cambiamento di contesto nel mondo del threading non è così economico nello scenario del mondo reale, in realtà è piuttosto costoso soprattutto quando hai più di 2 thread che devono essere calcolati e scambiati tra loro.

Nelle CPU moderne la situazione non è poi così diversa, il controller di memoria è integrato ma il mio punto è che una CPU x86 è fondamentalmente una macchina seriale e il controller di memoria funziona allo stesso modo della vecchia macchina con un controller di memoria esterno sulla scheda madre . Il cambio di contesto è ancora un costo rilevante nella mia applicazione e il fatto che il controller di memoria sia integrato o che la CPU più recente abbia più di 2 core non è un affare per me.

Per quello che ho sperimentato l'approccio concorrente è buono in teoria ma non così buono in pratica, con il modello di memoria imposto dall'hardware, è difficile fare un buon uso di questo paradigma, inoltre introduce molti problemi che vanno dall'uso delle mie strutture di dati al join di più thread.

Inoltre, entrambi i paradigmi non offrono alcun limite di sicurezza quando l'attività o il lavoro verranno eseguiti in un determinato momento, rendendoli davvero simili da un punto di vista funzionale.

Secondo il modello di memoria X86, perché la maggior parte delle persone suggerisce di utilizzare la concorrenza con C ++ e non solo un approccio asincrono? Inoltre, perché non considerare lo scenario peggiore di un computer in cui il cambio di contesto è probabilmente più costoso del calcolo stesso?


2
Un modo di confrontare sarebbe quello di guardare al mondo JavaScript, dove non c'è threading e tutto è aggressivamente asincrono, usando i callback. Funziona, ma ha i suoi problemi.
Gort il robot il

2
@StevenBurnap Come chiami i lavoratori del web?
user16764

2
"anche un numero relativamente basso di thread come 3-4-5 può essere un problema, l'applicazione non risponde ed è solo lenta e spiacevole." => Potrebbe essere dovuto al design scadente / uso inappropriato dei thread. In genere si trova quel tipo di situazione in cui i thread continuano a scambiare dati, nel qual caso il multi threading potrebbe non essere la risposta giusta o potrebbe essere necessario ripartizionare i dati.
Assylias,

1
@assylias Per vedere un significativo rallentamento nel thread dell'interfaccia utente indica una quantità eccessiva di blocco tra thread. O hai un'implementazione scadente o stai cercando di battere un piolo quadrato in un foro rotondo.
Evan Plaice,

5
Dici che "i programmatori sembrano amare la concorrenza e i programmi multi-thread in generale" Ne dubito. Direi "i programmatori lo odiano" ... ma spesso è l'unica cosa utile da fare ...
Johannes,

Risposte:


34

Si dispone di più core / procesors, utilizzare li

Async è la soluzione migliore per eseguire elaborazioni associate a IO pesanti ma per quanto riguarda l'elaborazione associata a CPU pesanti?

Il problema sorge quando i blocchi di codice a thread singolo (ovvero si bloccano) in un processo di lunga durata. Ad esempio, ricordi quando la stampa di un documento di elaboratore di testi avrebbe bloccato l'intera applicazione fino all'invio del lavoro? Il blocco delle applicazioni è un effetto collaterale di un blocco delle applicazioni a thread singolo durante un'attività ad alta intensità di CPU.

In un'applicazione multi-thread, le attività ad alta intensità di CPU (ad esempio un processo di stampa) possono essere inviate a un thread di lavoro in background liberando così il thread dell'interfaccia utente.

Allo stesso modo, in un'applicazione multi-processo il lavoro può essere inviato tramite messaggistica (ad es. IPC, socket, ecc.) A un sottoprocesso progettato specificamente per elaborare i lavori.

In pratica, il codice asincrono e multi-thread / processo hanno ciascuno i loro vantaggi e svantaggi.

Puoi vedere la tendenza nelle principali piattaforme cloud, in quanto offriranno istanze specializzate per l'elaborazione legata alla CPU e istanze specializzate per l'elaborazione legata all'IO.

Esempi:

  • Lo spazio di archiviazione (ex Amazon S3, Google Cloud Drive) è associato alla CPU
  • I server Web sono associati a IO (Amazon EC2, Google App Engine)
  • I database sono entrambi, CPU associato per scritture / indicizzazione e IO associato per letture

Per metterlo in prospettiva ...

Un server web è un perfetto esempio di piattaforma fortemente legata all'IO. Un server Web multi-thread che assegna un thread per connessione non si adatta bene perché ogni thread comporta un sovraccarico maggiore a causa della maggiore quantità di cambio di contesto e blocco dei thread sulle risorse condivise. Considerando che un server web asincrono userebbe un unico spazio di indirizzi.

Allo stesso modo, un'applicazione specializzata per la codifica di video funzionerebbe molto meglio in un ambiente multi-thread perché l'elaborazione pesante coinvolta avrebbe bloccato il thread principale fino al completamento del lavoro. Esistono modi per mitigarlo, ma è molto più semplice avere un singolo thread che gestisce una coda, un secondo thread che gestisce la pulizia e un pool di thread che gestiscono l'elaborazione pesante. La comunicazione tra i thread avviene solo quando le attività sono assegnate / completate, quindi l'overhead di blocco dei thread è ridotto al minimo.

L'applicazione migliore utilizza spesso una combinazione di entrambi. Una webapp, ad esempio, può utilizzare nginx (ovvero asincrono a thread singolo) come bilanciamento del carico per gestire il torrent di richieste in arrivo, un server web asincrono simile (ex Node.js) per gestire le richieste http e un set di server multi-thread gestire il caricamento / streaming / codifica dei contenuti, ecc ...

Nel corso degli anni ci sono state molte guerre di religione tra modelli multi-thread, multi-processo e asincroni. Come per la maggior parte delle cose, la migliore risposta dovrebbe essere davvero "dipende".

Segue una stessa linea di pensiero che giustifica l'uso parallelo di architetture GPU e CPU. Due sistemi specializzati in esecuzione in concerto possono avere un miglioramento molto maggiore rispetto a un singolo approccio monolitico.

Né sono migliori perché entrambi hanno i loro usi. Usa lo strumento migliore per il lavoro.

Aggiornare:

Ho rimosso il riferimento ad Apache e apportato una correzione minore. Apache utilizza un modello multiprocesso che prevede un processo per ogni richiesta aumentando la quantità di cambio di contesto a livello di kernel. Inoltre, poiché la memoria non può essere condivisa tra i processi, ogni richiesta comporta un costo di memoria aggiuntivo.

Il multi-threading si aggira richiedendo memoria aggiuntiva perché si basa su una memoria condivisa tra thread. La memoria condivisa rimuove l'overhead di memoria aggiuntiva ma comporta comunque la penalità di un maggiore cambio di contesto. Inoltre, per garantire che non si verifichino condizioni di competizione, sono necessari blocchi dei thread (che garantiscono l'accesso esclusivo a un solo thread alla volta) per tutte le risorse condivise tra i thread.

È divertente che tu dica "i programmatori sembrano amare la concorrenza e i programmi multi-thread in generale". La programmazione multi-thread è universalmente temuta da chiunque ne abbia fatto una quantità sostanziale nel suo tempo. I dead lock (un bug che si verifica quando una risorsa viene erroneamente bloccata da due diverse fonti che bloccano entrambi dal finire mai) e le condizioni di gara (in cui il programma genererà erroneamente il risultato errato in modo casuale a causa di un sequenziamento errato) sono alcuni dei più difficili da tracciare giù e sistemato.

Update2:

Contrariamente all'affermazione generale sul fatto che IPC è più veloce delle comunicazioni di rete (cioè socket). Non è sempre così . Tieni presente che si tratta di generalizzazioni e che i dettagli specifici dell'implementazione possono avere un impatto enorme sul risultato.


perché un programmatore dovrebbe passare a più processi? Voglio dire, suppongo che con più di un processo sia necessaria anche una sorta di comunicazione tra processi che possa aggiungere un notevole sovraccarico, è qualcosa di simile al vecchio modo di programmare di Windows? quando dovrei passare a più processi? Grazie per la tua risposta, comunque, un'ottima immagine di ciò che è asincrono e multi-thread.
user1849534

1
Si presuppone che la comunicazione tra processi aumenti l'overhead complessivo. Tuttavia, se lo stato di elaborazione è immutabile o deve solo gestire la sincronizzazione all'avvio / completamento. può essere molto più efficiente fan-out in attività più parallele. Il modello dell'attore è un buon esempio, e se non lo hai letto, vale davvero la pena di leggerlo. akka.io
sylvanaar,

1
@ user1849534 Più thread possono comunicare tra loro tramite memoria condivisa + blocco o IPC. Il blocco è più semplice ma più difficile da eseguire il debug se si commette un errore (ad esempio un blocco mancato, blocco morto). IPC è la soluzione migliore se hai molti thread di lavoro perché il blocco non si adatta bene. In entrambi i casi, se si utilizza un approccio multi-thread è importante mantenere la comunicazione / sincronizzazione tra thread al minimo assoluto (ovvero per ridurre al minimo l'overhead).
Evan Plaice,

1
@ akka.io Hai perfettamente ragione. L'immutabilità è un modo per minimizzare / eliminare il sovraccarico del blocco, ma si incorre comunque nel costo del cambio di contesto. Se desideri estendere la risposta per includere i dettagli su come l'immutabilità può risolvere i problemi di sincronizzazione dei thread, sentiti libero. Il punto principale che intendevo illustrare è che ci sono casi in cui la comunicazione asincrona ha un netto vantaggio rispetto al multi-thread / processo e viceversa.
Evan Plaice,

(cont) Ma, onestamente, se avessi bisogno di molte capacità di elaborazione legate alla CPU, salterei il modello attore e lo costruivo per adattarlo a più nodi di rete. La migliore soluzione che ho visto per questo è usare il modello di ventilatore di attività di 0MQ su comunicazioni a livello di socket. Vedi Fig 5 @ zguide.zeromq.org/page:all .
Evan Plaice,

13

L' approccio asincrono di Microsoft è un buon sostituto dello scopo più comune della programmazione multithread: migliorare la reattività rispetto alle attività di I / O.

Tuttavia, è importante rendersi conto che l'approccio asincrono non è in grado di migliorare le prestazioni o migliorare la reattività rispetto alle attività ad alta intensità di CPU.

Multithreading per reattività

Il multithreading per la reattività è il modo tradizionale per mantenere reattivo un programma durante attività di I / O pesanti o attività di calcolo pesanti. I file vengono salvati su un thread in background, in modo che l'utente possa continuare il proprio lavoro, senza dover attendere che il disco rigido completi il ​​proprio compito. Il thread IO spesso blocca l'attesa del completamento di una parte di una scrittura, quindi i cambi di contesto sono frequenti.

Allo stesso modo, quando si esegue un calcolo complesso, si desidera consentire il cambio di contesto regolare in modo che l'interfaccia utente possa rimanere reattiva e l'utente non pensa che il programma sia andato in crash.

L'obiettivo qui non è, in generale, far funzionare i thread multipli su CPU diverse. Invece, siamo solo interessati a far sì che si verifichino cambi di contesto tra l'attività in background di lunga durata e l'interfaccia utente, in modo che l'interfaccia utente sia in grado di aggiornare e rispondere all'utente mentre l'attività in background è in esecuzione. In generale, l'interfaccia utente non assorbirà molta potenza della CPU e il framework di threading o il sistema operativo generalmente decideranno di eseguirli sulla stessa CPU.

In realtà perdiamo le prestazioni complessive a causa del costo aggiuntivo del cambio di contesto, ma non ci interessa perché le prestazioni della CPU non erano il nostro obiettivo. Sappiamo che di solito abbiamo più potenza della CPU di cui abbiamo bisogno, e quindi il nostro obiettivo per quanto riguarda il multithreading è quello di svolgere un'attività per l'utente senza perdere tempo.

L'alternativa "asincrona"

L '"approccio asincrono" cambia questa immagine abilitando i cambi di contesto all'interno di un singolo thread. Ciò garantisce che tutte le nostre attività verranno eseguite su una singola CPU e potrebbe fornire alcuni modesti miglioramenti delle prestazioni in termini di minore creazione / pulizia dei thread e meno cambi di contesto reali tra i thread.

Invece di creare un nuovo thread in attesa della ricezione di una risorsa di rete (ad esempio il download di un'immagine), asyncviene utilizzato un metodo, che awaitdiventa l'immagine disponibile e, nel frattempo, cede al metodo di chiamata.

Il vantaggio principale qui è che non devi preoccuparti di problemi di threading come evitare deadlock, poiché non stai usando i blocchi e la sincronizzazione, e c'è un po 'meno lavoro per il programmatore che imposta il thread in background e torna indietro sul thread dell'interfaccia utente quando il risultato ritorna per aggiornare l'interfaccia utente in modo sicuro.

Non ho esaminato troppo a fondo i dettagli tecnici, ma la mia impressione è che la gestione del download con attività della CPU occasionale leggera diventi un'attività non per un thread separato, ma piuttosto qualcosa di più simile a un'attività sulla coda degli eventi dell'interfaccia utente e quando al termine del download, il metodo asincrono viene ripreso da quella coda di eventi. In altre parole, awaitsignifica qualcosa di simile a "controllare se il risultato di cui ho bisogno è disponibile, in caso contrario, rimettermi nella coda delle attività di questo thread".

Si noti che questo approccio non risolverebbe il problema di un'attività ad alta intensità di CPU: non ci sono dati da attendere, quindi non possiamo ottenere i cambi di contesto necessari per creare senza un vero thread di lavoro in background. Naturalmente, potrebbe essere comunque conveniente utilizzare un metodo asincrono per avviare il thread in background e restituire il risultato, in un programma che utilizza pervasivamente l'approccio asincrono.

Multithreading per prestazioni

Dato che parli di "prestazioni", vorrei anche discutere di come il multithreading può essere utilizzato per ottenere prestazioni migliori, cosa del tutto impossibile con l'approccio asincrono a thread singolo.

Quando sei effettivamente in una situazione in cui non hai abbastanza potenza della CPU su una singola CPU e vuoi usare il multithreading per le prestazioni, in realtà è spesso difficile da fare. D'altra parte, se una CPU non è abbastanza potente per l'elaborazione, spesso è anche l'unica soluzione che potrebbe consentire al programma di fare ciò che vorresti realizzare in un lasso di tempo ragionevole, che è ciò che rende utile il lavoro.

Parallelismo fondamentale

Certo, a volte può essere facile ottenere un vero speedup dal multithreading.

Se ti capita di avere un gran numero di attività indipendenti ad alta intensità di calcolo (vale a dire attività i cui dati di input e output sono molto piccoli rispetto ai calcoli che devono essere eseguiti per determinare il risultato), allora puoi spesso ottenere una notevole accelerazione di creando un pool di thread (dimensionato in modo appropriato in base al numero di CPU disponibili) e facendo in modo che un thread master distribuisca il lavoro e raccolga i risultati.

Multithreading pratico per prestazioni

Non voglio presentarmi come troppo esperto, ma la mia impressione è che, in generale, il multithreading più pratico per le prestazioni che si verificano in questi giorni è alla ricerca di luoghi in un'applicazione che abbia un banale parallelismo e usando più thread per raccogliere i benefici.

Come con qualsiasi ottimizzazione, di solito è meglio ottimizzare dopo aver profilato le prestazioni del programma e identificato i punti critici: è facile rallentare un programma decidendo arbitrariamente che questa parte dovrebbe essere eseguita in un thread e quella in un altro, senza determinare innanzitutto se entrambe le parti occupano una parte significativa del tempo della CPU.

Un thread aggiuntivo significa più costi di installazione / smontaggio e più switch di contesto o più costi di comunicazione tra CPU. Se non sta facendo abbastanza lavoro per compensare quei costi se su una CPU separata e non ha bisogno di essere un thread separato per motivi di reattività, rallenterà le cose senza alcun vantaggio.

Cerca attività con poche interdipendenze e che occupano una parte significativa del tempo di esecuzione del tuo programma.

Se non hanno interdipendenze, allora è un caso di banale parallelismo, puoi facilmente impostare ciascuno con un thread e goderne i vantaggi.

Se riesci a trovare attività con interdipendenza limitata, in modo che il blocco e la sincronizzazione per lo scambio di informazioni non li rallentino in modo significativo, il multithreading può dare una certa velocità, a condizione che tu stia attento a evitare i pericoli di deadlock dovuti a logica difettosa durante la sincronizzazione o risultati errati dovuti alla mancata sincronizzazione quando è necessario.

In alternativa, alcune delle applicazioni più comuni per il multithreading non sono (in un certo senso) alla ricerca di accelerazione di un algoritmo predeterminato, ma invece di un budget più ampio per l'algoritmo che stanno pianificando di scrivere: se stai scrivendo un motore di gioco e la tua intelligenza artificiale deve prendere una decisione all'interno della frequenza dei fotogrammi, spesso puoi assegnare alla tua intelligenza artificiale un budget maggiore per il ciclo della CPU se puoi fornirle una CPU propria.

Tuttavia, assicurati di profilare i thread e assicurati che stiano facendo abbastanza lavoro per compensare il costo a un certo punto.

Algoritmi paralleli

Ci sono anche molti problemi che possono essere velocizzati usando più processori, ma che sono troppo monolitici per essere semplicemente divisi tra le CPU.

Gli algoritmi paralleli devono essere attentamente analizzati per i loro runtime di Big O rispetto al miglior algoritmo non parallelo disponibile, poiché è molto facile per i costi di comunicazione tra CPU eliminare qualsiasi vantaggio derivante dall'utilizzo di più CPU. In generale, devono utilizzare meno comunicazioni tra CPU (in termini di big-O) di quanto non utilizzino i calcoli su ciascuna CPU.

Al momento, è ancora in gran parte uno spazio per la ricerca accademica, in parte a causa della complessa analisi richiesta, in parte perché il banale parallelismo è abbastanza comune, in parte perché non abbiamo ancora così tanti core CPU sui nostri computer che problemi che non può essere risolto in un lasso di tempo ragionevole su una CPU potrebbe essere risolto in un lasso di tempo ragionevole utilizzando tutte le nostre CPU.


+1 per una risposta ovviamente ben ponderata. Però presterei attenzione a prendere i suggerimenti di Microsoft al valore nominale. Tieni presente che .NET è una piattaforma sincrona, quindi l'ecosistema è propenso a fornire migliori strutture / documentazione a supporto della creazione di soluzioni sincrone. Il contrario sarebbe vero per una piattaforma asincrona come Node.js.
Evan Plaice,

3

l'applicazione non risponde ed è solo lenta e spiacevole.

E c'è il tuo problema. Un'interfaccia utente reattiva non crea un'applicazione performante. Spesso il contrario. Un sacco di tempo viene speso per controllare l'input dell'interfaccia utente piuttosto che fare in modo che i thread di lavoro eseguano il loro lavoro.

Per quanto "solo" abbia un approccio asincrono, è multithreading anche se ottimizzato per quel particolare caso d'uso nella maggior parte degli ambienti . In altri, quell'asincrono viene fatto tramite coroutine che non sono ... sempre simultanee.

Francamente, trovo che le operazioni asincrone siano più difficili da ragionare e utilizzare in un modo che effettivamente fornisce vantaggi (prestazioni, robustezza, manutenibilità) anche rispetto a ... più approcci manuali.


perché ? per esempio cosa trovi così banane nella libreria boost signal2?
user1849534
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.