Qual è la differenza tra programmazione asincrona e multithreading?


234

Pensavo che fossero fondamentalmente la stessa cosa: scrivere programmi che dividessero le attività tra processori (su macchine con più di 2 processori). Quindi sto leggendo questo , che dice:

I metodi asincroni sono intesi come operazioni non bloccanti. Un'espressione waitit in un metodo asincrono non blocca il thread corrente mentre il task atteso è in esecuzione. Invece, l'espressione registra il resto del metodo come continuazione e restituisce il controllo al chiamante del metodo asincrono.

Le parole chiave asincrone e attendono non causano la creazione di thread aggiuntivi. I metodi asincroni non richiedono il multithreading perché un metodo asincrono non viene eseguito sul proprio thread. Il metodo viene eseguito nel contesto di sincronizzazione corrente e utilizza l'ora sul thread solo quando il metodo è attivo. È possibile utilizzare Task.Run per spostare il lavoro associato alla CPU in un thread in background, ma un thread in background non aiuta con un processo che sta solo aspettando che i risultati diventino disponibili.

e mi chiedo se qualcuno può tradurlo in inglese per me. Sembra fare una distinzione tra asincronismo (è una parola?) E threading e implica che si può avere un programma che ha compiti asincroni ma non multithreading.

Ora capisco l'idea di compiti asincroni come l'esempio a pag. 467 di C # In Depth, terza edizione di Jon Skeet

async void DisplayWebsiteLength ( object sender, EventArgs e )
{
    label.Text = "Fetching ...";
    using ( HttpClient client = new HttpClient() )
    {
        Task<string> task = client.GetStringAsync("http://csharpindepth.com");
        string text = await task;
        label.Text = text.Length.ToString();
    }
}

La asyncparola chiave significa " Questa funzione, ogni volta che viene chiamata, non verrà chiamata in un contesto in cui è richiesto il suo completamento affinché tutto dopo la sua chiamata sia chiamato".

In altre parole, scriverlo nel mezzo di un compito

int x = 5; 
DisplayWebsiteLength();
double y = Math.Pow((double)x,2000.0);

, poiché DisplayWebsiteLength()non ha nulla a che fare con xo y, verrà DisplayWebsiteLength()eseguito "in background", come

                processor 1                |      processor 2
-------------------------------------------------------------------
int x = 5;                                 |  DisplayWebsiteLength()
double y = Math.Pow((double)x,2000.0);     |

Ovviamente questo è un esempio stupido, ma ho ragione o sono totalmente confuso o cosa?

(Inoltre, sono confuso sul perché sendere enon sono mai utilizzati nel corpo della funzione sopra.)



sendere estanno suggerendo che questo è in realtà un gestore di eventi - praticamente l'unico posto dove async voidè desiderabile. Molto probabilmente, questo viene chiamato con un clic sul pulsante o qualcosa del genere - il risultato è che questa azione avviene completamente in modo asincrono rispetto al resto dell'applicazione. Ma è ancora tutto su un thread: il thread dell'interfaccia utente (con un piccolo intervallo di tempo su un thread IOCP che pubblica il callback nel thread dell'interfaccia utente).
Luaan,


3
Una nota molto importante sull'esempio di DisplayWebsiteLengthcodice: non si deve usare HttpClientin usingun'istruzione - Sotto un carico pesante, il codice può esaurire il numero di socket disponibili causando errori SocketException. Maggiori informazioni sull'istanza impropria .
Gan,

1
@JakubLortz Non so per chi l'articolo sia davvero. Non per i principianti, poiché richiede una buona conoscenza di thread, interrupt, elementi relativi alla CPU, ecc. Non per utenti esperti, poiché per loro è già tutto chiaro. Sono sicuro che non aiuterà nessuno a capire di cosa si tratta - un livello di astrazione troppo elevato.
Loreno,

Risposte:


589

Il tuo malinteso è estremamente comune. A molte persone viene insegnato che il multithreading e l'asincronia sono la stessa cosa, ma non lo sono.

Un'analogia di solito aiuta. Stai cucinando in un ristorante. Arriva un ordine per uova e toast.

  • Sincrono: cuoci le uova, poi cuoci il toast.
  • Asincrono, a filo singolo: inizi la cottura delle uova e imposti un timer. Si avvia la cottura dei toast e si imposta un timer. Mentre entrambi cucinano, pulisci la cucina. Quando i timer si spengono, togli le uova dal fuoco e il toast fuori dal tostapane e servili.
  • Asincrono, multithread: assumi altri due cuochi, uno per cucinare le uova e uno per cucinare i toast. Ora hai il problema di coordinare i cuochi in modo che non siano in conflitto tra loro in cucina durante la condivisione delle risorse. E devi pagarli.

Ora ha senso che il multithreading è solo un tipo di asincronia? Il threading riguarda i lavoratori; l'asincronia riguarda le attività . Nei flussi di lavoro con multithreading si assegnano attività ai lavoratori. Nei flussi di lavoro asincroni a thread singolo è disponibile un grafico delle attività in cui alcune attività dipendono dai risultati di altre; man mano che ogni attività viene completata, viene richiamato il codice che pianifica l'attività successiva che può essere eseguita, dati i risultati dell'attività appena completata. Ma (si spera) hai solo bisogno di un lavoratore per eseguire tutte le attività, non un lavoratore per attività.

Aiuterà a capire che molte attività non sono legate al processore. Per le attività associate al processore ha senso assumere tutti i lavoratori (thread) quanti sono i processori, assegnare un'attività a ciascun lavoratore, assegnare un processore a ciascun lavoratore e fare in modo che ciascun processore svolga il compito di nient'altro che calcolare il risultato come il più rapidamente possibile. Ma per le attività che non sono in attesa su un processore, non è necessario assegnare affatto un lavoratore. Aspetti solo che arrivi il messaggio che il risultato è disponibile e fai qualcos'altro mentre aspetti . Quando arriva quel messaggio, puoi pianificare la continuazione dell'attività completata come la prossima cosa da fare sulla tua lista di cose da fare.

Quindi diamo un'occhiata all'esempio di Jon in modo più dettagliato. Che succede?

  • Qualcuno invoca DisplayWebSiteLength. Oms? Non ci interessa.
  • Imposta un'etichetta, crea un client e chiede al client di recuperare qualcosa. Il client restituisce un oggetto che rappresenta l'attività di recupero di qualcosa. Tale compito è in corso.
  • È in corso su un altro thread? Probabilmente no. Leggi l'articolo di Stephen sul perché non ci sono discussioni.
  • Ora attendiamo il compito. Che succede? Controlliamo per vedere se l'attività è stata completata tra il momento in cui l'abbiamo creata e l'abbiamo aspettata. Se sì, prendiamo il risultato e continuiamo a correre. Supponiamo che non sia stato completato. Registriamo il resto di questo metodo come continuazione di quell'attività e ritorno .
  • Ora il controllo è tornato al chiamante. Che cosa fa? Qualunque cosa voglia.
  • Supponiamo ora che l'attività venga completata. Come ha fatto? Forse era in esecuzione su un altro thread o forse il chiamante a cui siamo appena tornati gli ha permesso di completarlo sul thread corrente. Indipendentemente da ciò, ora abbiamo un'attività completata.
  • L'attività completata richiede al thread corretto - di nuovo, probabilmente l' unico thread - di eseguire la continuazione dell'attività.
  • Il controllo passa immediatamente al metodo che abbiamo appena lasciato nel punto di attesa. Ora è disponibile un risultato in modo che possiamo assegnare texted eseguire il resto del metodo.

È proprio come nella mia analogia. Qualcuno ti chiede un documento. Si invia via posta per il documento e si continua a fare altri lavori. Quando arriva nella posta ti viene segnalato e quando ne hai voglia, fai il resto del flusso di lavoro: apri la busta, paghi le spese di consegna, qualunque cosa. Non è necessario assumere un altro lavoratore per fare tutto questo per te.


8
@ user5648283: l'hardware è il livello sbagliato per pensare alle attività. Un'attività è semplicemente un oggetto che (1) rappresenta che un valore sarà disponibile in futuro e (2) può eseguire il codice (sul thread corretto) quando tale valore è disponibile . Come dipende da ogni singola attività il risultato in futuro. Alcuni useranno hardware speciale come "dischi" e "schede di rete" per farlo; alcuni useranno hardware come le CPU.
Eric Lippert,

13
@ user5648283: Ancora una volta, pensa alla mia analogia. Quando qualcuno ti chiede di cucinare uova e toast, usi hardware speciale - una stufa e un tostapane - e puoi pulire la cucina mentre l'hardware sta facendo il suo lavoro. Se qualcuno ti chiede uova, toast e una critica originale dell'ultimo film di Hobbit, puoi scrivere la tua recensione mentre le uova e il toast stanno cucinando, ma non è necessario utilizzare l'hardware per questo.
Eric Lippert,

9
@ user5648283: Ora, per quanto riguarda la tua domanda su "riorganizzare il codice", considera questo. Supponiamo di avere un metodo P che ha un rendimento e un metodo Q che fa un foreach sul risultato di P. Passa attraverso il codice. Vedrai che eseguiamo un po 'di Q, poi un po' di P, poi un po 'di Q ... Capisci il punto? l'attesa è essenzialmente resa resa in costume . Adesso è più chiaro?
Eric Lippert,

10
Il tostapane è hardware. L'hardware non ha bisogno di un thread per ripararlo; dischi e schede di rete e quant'altro non funziona a un livello molto inferiore a quello dei thread del sistema operativo.
Eric Lippert,

5
@ShivprasadKoirala: Questo non è assolutamente vero . Se ci credi, allora hai delle credenze molto false sull'asincronia . Il punto fondamentale dell'asincronia in C # è che non crea un thread.
Eric Lippert

27

Nel browser Javascript è un ottimo esempio di programma asincrono che non ha thread.

Non devi preoccuparti di più parti di codice che toccano gli stessi oggetti contemporaneamente: ogni funzione finirà di essere eseguita prima che qualsiasi altro javascript sia autorizzato a funzionare sulla pagina.

Tuttavia, quando si esegue qualcosa come una richiesta AJAX, nessun codice è in esecuzione, quindi altri javascript possono rispondere a cose come gli eventi click fino a quando quella richiesta non ritorna e invoca il callback ad esso associato. Se uno di questi altri gestori di eventi è ancora in esecuzione quando viene restituita la richiesta AJAX, il relativo gestore non verrà chiamato fino a quando non viene completato. C'è solo un "thread" JavaScript in esecuzione, anche se è possibile mettere in pausa efficacemente ciò che stavi facendo fino a quando non hai le informazioni di cui hai bisogno.

Nelle applicazioni C #, la stessa cosa accade ogni volta che hai a che fare con gli elementi dell'interfaccia utente: puoi interagire con gli elementi dell'interfaccia utente solo quando sei nel thread dell'interfaccia utente. Se l'utente ha fatto clic su un pulsante e si desidera rispondere leggendo un file di grandi dimensioni dal disco, un programmatore inesperto potrebbe commettere l'errore di leggere il file all'interno del gestore dell'evento click stesso, causando il "blocco" dell'applicazione fino al il caricamento del file è terminato perché non è consentito rispondere ad altri clic, hovering o altri eventi relativi all'interfaccia utente fino a quando il thread non viene liberato.

Un'opzione che i programmatori potrebbero usare per evitare questo problema è quella di creare un nuovo thread per caricare il file e quindi dire al codice di quel thread che quando il file viene caricato deve eseguire nuovamente il codice rimanente sul thread dell'interfaccia utente in modo che possa aggiornare gli elementi dell'interfaccia utente in base a ciò che ha trovato nel file. Fino a poco tempo fa, questo approccio era molto popolare perché era ciò che rendeva facili le librerie e il linguaggio C #, ma è fondamentalmente più complicato di quanto debba essere.

Se pensi a cosa sta facendo la CPU quando legge un file a livello di hardware e sistema operativo, in pratica sta dando un'istruzione per leggere pezzi di dati dal disco in memoria e colpire il sistema operativo con un "interrupt "quando la lettura è completa. In altre parole, leggere dal disco (o qualsiasi I / O realmente) è intrinsecamente asincrona . Il concetto di thread che attende il completamento dell'I / O è un'astrazione creata dagli sviluppatori della libreria per semplificare la programmazione. Non è necessario.

Ora, la maggior parte delle operazioni di I / O in .NET hanno un corrispondente ...Async() metodo che puoi invocare, che restituisce Taskquasi immediatamente. È possibile aggiungere richiamate a questo Taskper specificare il codice che si desidera eseguire al completamento dell'operazione asincrona. Puoi anche specificare su quale thread vuoi che quel codice venga eseguito e puoi fornire un token che l'operazione asincrona può controllare di volta in volta per vedere se hai deciso di annullare l'attività asincrona, dandogli l'opportunità di interrompere rapidamente il suo lavoro e con grazia.

Fino all'aggiunta delle async/awaitparole chiave, C # era molto più ovvio su come viene invocato il codice di callback, poiché tali callback erano sotto forma di delegati associati all'attività. Per darti ancora il vantaggio di usare il...Async() operazione, evitando la complessità del codice, async/awaitimpedisce la creazione di quei delegati. Ma sono ancora lì nel codice compilato.

Quindi puoi avere il tuo gestore di eventi dell'interfaccia utente awaitun'operazione I / O, liberare il thread dell'interfaccia utente per fare altre cose e, più o meno, tornare automaticamente al thread dell'interfaccia utente una volta terminata la lettura del file, senza mai dover crea una nuova discussione.


Esiste un solo "thread" JavaScript in esecuzione , non più vero con Web Workers .
oleksii

6
@oleksii: tecnicamente è vero, ma non ci pensavo perché l'API Web Workers stessa è asincrona e ai Web Worker non è consentito influenzare direttamente i valori javascript o il DOM sulla pagina Web che vengono invocati da, il che significa che il secondo paragrafo cruciale di questa risposta è ancora valido. Dal punto di vista del programmatore, c'è poca differenza tra invocare un Web Worker e invocare una richiesta AJAX.
StriplingWarrior
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.