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), async
viene utilizzato un metodo, che await
diventa 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, await
significa 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.