Se async-waitit non crea thread aggiuntivi, come può rendere le applicazioni reattive?


242

Di volta in volta, vedo che si dice che l'uso di async- awaitnon crea alcun thread aggiuntivo. Questo non ha senso perché l'unico modo in cui un computer può sembrare fare più di una cosa alla volta è

  • Effettivamente facendo più di 1 cosa alla volta (eseguendo in parallelo, facendo uso di più processori)
  • Simulandolo programmando le attività e passando da una all'altra (fai un po 'di A, un po' di B, un po 'di A, ecc.)

Quindi se async- awaitnessuno dei due funziona, come può rendere reattiva un'applicazione? Se esiste solo 1 thread, quindi chiamare un metodo significa attendere il completamento del metodo prima di fare qualsiasi altra cosa, ei metodi all'interno di quel metodo devono attendere il risultato prima di procedere, e così via.


17
Le attività IO non sono associate alla CPU e quindi non richiedono un thread. Il punto principale di asincrono è non bloccare i thread durante le attività legate all'IO.
juharr,

24
@jdweng: No, per niente. Anche se ha creato nuovi thread , è molto diverso dalla creazione di un nuovo processo.
Jon Skeet,

8
Se capisci la programmazione asincrona basata su callback, capisci come await/ asyncfunziona senza creare thread.
user253751,

6
Non è esattamente rendere un'applicazione più reattivo, ma non ti scoraggiare di bloccare i thread, che è una causa comune di applicazioni che non rispondono.
Owen,

6
@RubberDuck: Sì, potrebbe utilizzare un thread dal pool di thread per la continuazione. Ma non sta iniziando un thread nel modo in cui l'OP immagina qui - non è come dice "Prendi questo metodo ordinario, ora eseguilo in un thread separato - lì, è asincrono." È molto più sottile di così.
Jon Skeet,

Risposte:


299

In realtà, asincrono / aspettano non è così magico. L'intero argomento è piuttosto ampio, ma per una risposta abbastanza rapida ma completa alla tua domanda penso che possiamo farcela.

Affrontiamo un semplice evento di clic sul pulsante in un'applicazione Windows Form:

public async void button1_Click(object sender, EventArgs e)
{
    Console.WriteLine("before awaiting");
    await GetSomethingAsync();
    Console.WriteLine("after awaiting");
}

Io vado a esplicitamente non parlare di ciò si GetSomethingAsyncsta tornando per ora. Diciamo solo che questo è qualcosa che si completerà dopo, diciamo, 2 secondi.

In un mondo tradizionale, non asincrono, il gestore di eventi di clic sui pulsanti sarebbe simile al seguente:

public void button1_Click(object sender, EventArgs e)
{
    Console.WriteLine("before waiting");
    DoSomethingThatTakes2Seconds();
    Console.WriteLine("after waiting");
}

Quando si fa clic sul pulsante nel modulo, l'applicazione sembrerà bloccarsi per circa 2 secondi, mentre attendiamo il completamento di questo metodo. Quello che succede è che il "pump dei messaggi", sostanzialmente un loop, è bloccato.

Questo ciclo chiede continuamente a Windows "Qualcuno ha fatto qualcosa, come spostare il mouse, fare clic su qualcosa? Devo ridipingere qualcosa? Se sì, dimmelo!" e quindi elabora quel "qualcosa". Questo ciclo ha ricevuto un messaggio che l'utente ha fatto clic su "button1" (o il tipo equivalente di messaggio da Windows) e ha finito per chiamare il nostro button1_Clickmetodo sopra. Fino a quando questo metodo non ritorna, questo loop è ora bloccato in attesa. Questo richiede 2 secondi e durante questo, nessun messaggio viene elaborato.

La maggior parte delle cose che si occupano di Windows sono fatte usando i messaggi, il che significa che se il ciclo di messaggi smette di pompare i messaggi, anche solo per un secondo, è rapidamente evidente dall'utente. Ad esempio, se sposti il ​​blocco note o qualsiasi altro programma sopra il tuo programma e poi di nuovo via, una serie di messaggi di disegno vengono inviati al tuo programma indicando quale regione della finestra che ora è diventata improvvisamente di nuovo visibile. Se il ciclo di messaggi che elabora questi messaggi è in attesa di qualcosa, bloccato, allora non viene eseguito alcun disegno.

Quindi, se nel primo esempio, async/awaitnon crea nuovi thread, come lo fa?

Bene, quello che succede è che il tuo metodo è diviso in due. Questo è uno di quegli argomenti di ampio tipo, quindi non entrerò in troppi dettagli ma basti dire che il metodo è suddiviso in queste due cose:

  1. Tutto il codice che porta a await, compresa la chiamata aGetSomethingAsync
  2. Tutto il codice seguente await

Illustrazione:

code... code... code... await X(); ... code... code... code...

riarrangiato:

code... code... code... var x = X(); await X; code... code... code...
^                                  ^          ^                     ^
+---- portion 1 -------------------+          +---- portion 2 ------+

Fondamentalmente il metodo viene eseguito in questo modo:

  1. Esegue tutto fino a await
  2. Chiama il GetSomethingAsyncmetodo, che fa la sua cosa, e restituisce qualcosa che completerà 2 secondi in futuro

    Finora siamo ancora all'interno della chiamata originale a button1_Click, che si verifica sul thread principale, chiamato dal loop dei messaggi. Se il codice che precede awaitrichiede molto tempo, l'interfaccia utente verrà comunque bloccata. Nel nostro esempio, non così tanto

  3. Quello che la awaitparola chiave, insieme ad una magica intelligenza del compilatore, è che fondamentalmente è qualcosa del tipo "Ok, sai cosa, tornerò semplicemente dal gestore di eventi clic sul pulsante qui. Quando tu (come in, la cosa che sto aspettando) vai al completamento, fammi sapere perché mi resta ancora del codice da eseguire ".

    In realtà, farà sapere alla classe SynchronizationContext che è fatto, che, a seconda del contesto di sincronizzazione attuale che è in gioco in questo momento, farà la fila per l'esecuzione. La classe di contesto utilizzata in un programma Windows Form la metterà in coda utilizzando la coda che il ciclo di messaggi sta pompando.

  4. Quindi ritorna al ciclo dei messaggi, che ora è libero di continuare a pompare i messaggi, come spostare la finestra, ridimensionarla o fare clic su altri pulsanti.

    Per l'utente, l'interfaccia utente ora è di nuovo reattiva, elaborando altri clic sui pulsanti, ridimensionando e, soprattutto, ridisegnando , quindi non sembra bloccarsi.

  5. 2 secondi dopo, la cosa che stiamo aspettando viene completata e ciò che accade ora è che (beh, il contesto di sincronizzazione) inserisce un messaggio nella coda che il ciclo di messaggi sta guardando, dicendo "Ehi, ho dell'altro codice per eseguire ", e questo codice è tutto il codice dopo l'attesa.
  6. Quando il ciclo di messaggi arriva a quel messaggio, fondamentalmente "reinserirà" quel metodo da cui era stato interrotto, subito dopo awaite continuando a eseguire il resto del metodo. Si noti che questo codice viene nuovamente chiamato dal ciclo di messaggi, quindi se questo codice fa qualcosa di lungo senza usarlo async/awaitcorrettamente, bloccherà nuovamente il ciclo di messaggi

Ci sono molte parti mobili sotto il cofano qui, quindi qui ci sono alcuni collegamenti a ulteriori informazioni, stavo per dire "se ne avessi bisogno", ma questo argomento è piuttosto ampio ed è abbastanza importante conoscere alcune di quelle parti mobili . Invariabilmente capirai che async / wait è ancora un concetto che perde. Alcune delle limitazioni e dei problemi sottostanti continuano a insinuarsi nel codice circostante e, in caso contrario, di solito si finisce per eseguire il debug di un'applicazione che si interrompe in modo casuale per apparentemente senza una buona ragione.


OK, quindi cosa succede se GetSomethingAsyncgira un thread che verrà completato in 2 secondi? Sì, allora ovviamente c'è un nuovo thread in gioco. Questo thread, tuttavia, non è a causa dell'asincrono di questo metodo, ma perché il programmatore di questo metodo ha scelto un thread per implementare il codice asincrono. Quasi tutti gli I / O asincroni non usano un thread, usano cose diverse. async/await da soli non creano nuovi thread ma ovviamente le "cose ​​che aspettiamo" possono essere implementate usando i thread.

Ci sono molte cose in .NET che non fanno necessariamente girare un thread da sole ma sono ancora asincrone:

  • Richieste Web (e molte altre cose relative alla rete che richiedono tempo)
  • Lettura e scrittura di file asincroni
  • e molti altri, un buon segno è se la classe / interfaccia in questione ha metodi denominati SomethingSomethingAsynco BeginSomethinged EndSomethinge c'è un IAsyncResultcoinvolto.

Di solito queste cose non usano un filo sotto il cappuccio.


OK, quindi vuoi alcune di quelle "cose ​​a tema ampio"?

Bene, chiediamo a Prova Roslyn del nostro clic sul pulsante:

Prova Roslyn

Non ho intenzione di collegarmi alla classe completamente generata qui, ma è roba piuttosto cruenta.


11
Quindi è fondamentalmente ciò che l'OP ha descritto come " Simulare l'esecuzione parallela programmando le attività e passando da una all'altra ", non è vero?
Bergi,

4
@Bergi Non proprio. L'esecuzione è veramente parallela - l'attività di I / O asincrona è in corso e non richiede alcun thread per procedere (questo è qualcosa che è stato usato molto prima dell'arrivo di Windows - MS DOS ha usato anche l'I / O asincrono, anche se non hanno multi-thread!). Certo, await può essere usato anche nel modo in cui lo descrivi, ma generalmente non lo è. Sono pianificati solo i callback (nel pool di thread): tra il callback e la richiesta non è necessario alcun thread.
Luaan,

3
Ecco perché volevo evitare esplicitamente di parlare troppo di ciò che quel metodo ha fatto, poiché la domanda riguardava in particolare asincronizzazione / attesa, che non crea i propri thread. Ovviamente, possono essere utilizzati per attendere per le discussioni da completare.
Lasse V. Karlsen,

6
@ LasseV.Karlsen - Sto ingerendo la tua grande risposta, ma sono ancora attaccato a un dettaglio. Comprendo che esiste il gestore eventi, come nel passaggio 4, che consente al pump dei messaggi di continuare a pompare, ma quando e dove continua la "cosa che richiede due secondi" se non su un thread separato? Se dovesse essere eseguito sul thread dell'interfaccia utente, bloccherebbe comunque il pump dei messaggi mentre è in esecuzione perché deve eseguire un po 'di tempo sullo stesso thread .. [continua] ...
rory.ap

3
Mi piace la tua spiegazione con il pump dei messaggi. In che modo la tua spiegazione differisce quando non esiste un pump dei messaggi come nell'applicazione console o nel web server? Come si ottiene il rientro di un metodo?
Puchacz,

95

Lo spiego per intero nel mio post sul blog There Is No Thread .

In sintesi, i moderni sistemi I / O fanno un uso intensivo del DMA (Direct Memory Access). Esistono processori speciali dedicati su schede di rete, schede video, controller HDD, porte seriali / parallele, ecc. Questi processori hanno accesso diretto al bus di memoria e gestiscono la lettura / scrittura in modo completamente indipendente dalla CPU. La CPU deve solo notificare al dispositivo la posizione in memoria contenente i dati e quindi può fare da sola fino a quando il dispositivo non genera un interrupt notificando alla CPU che la lettura / scrittura è completa.

Una volta che l'operazione è in volo, non c'è lavoro da fare per la CPU e quindi nessun thread.


Giusto per chiarire .. Comprendo l'alto livello di ciò che accade quando si utilizza async-waitit. Per quanto riguarda la creazione di un thread senza thread - non esiste thread solo nelle richieste I / O a dispositivi che come hai detto hanno i propri processori che gestiscono la richiesta stessa? Possiamo supporre che TUTTE le richieste I / O siano gestite su tali processori indipendenti, il che significa utilizzare Task. Eseguire SOLO su azioni associate alla CPU?
Yonatan Nir,

@YonatanNir: non si tratta solo di processori separati; qualsiasi tipo di risposta guidata dagli eventi è naturalmente asincrono. Task.Runè più appropriato per le azioni legate alla CPU , ma ha anche una manciata di altri usi.
Stephen Cleary,

1
Ho finito di leggere il tuo articolo e c'è ancora qualcosa di fondamentale che non capisco poiché non ho molta familiarità con l'implementazione di livello inferiore del sistema operativo. Ho ottenuto quello che hai scritto fino a dove hai scritto: "L'operazione di scrittura è ora" in volo ". Quanti thread lo stanno elaborando? Nessuno." . Quindi, se non ci sono thread, come fa l'operazione stessa se non su un thread?
Yonatan Nir,

6
Questo è il pezzo mancante in migliaia di spiegazioni !!! In realtà c'è qualcuno che fa il lavoro in background con operazioni di I / O. Non è un thread ma un altro componente hardware dedicato che fa il suo lavoro!
the_dark_destructor il

2
@PrabuWeerasinghe: il compilatore crea una struttura che contiene lo stato e le variabili locali. Se un attesa ha bisogno di cedere (cioè tornare al suo chiamante), quella struttura è inscatolata e vive nell'heap.
Stephen Cleary,

87

gli unici modi in cui sembra che un computer stia facendo più di 1 cosa alla volta è (1) In realtà fare più di 1 cosa alla volta, (2) simularlo pianificando le attività e passando da una all'altra. Quindi se async-waitit non fa nessuno di questi

Non è che aspettare non faccia nessuno di questi. Ricorda, lo scopo di awaitnon è rendere magicamente asincrono il codice sincrono . Serve per usare le stesse tecniche che usiamo per scrivere codice sincrono quando si chiama in codice asincrono . Aspettare di fare in modo che il codice che utilizza operazioni ad alta latenza assomigli al codice che utilizza operazioni a bassa latenza . Quelle operazioni ad alta latenza potrebbero essere sui thread, potrebbero essere su hardware per scopi speciali, potrebbero strappare il loro lavoro in piccoli pezzi e metterlo nella coda dei messaggi per l'elaborazione in seguito dal thread dell'interfaccia utente. Stanno facendo qualcosa per raggiungere l'asincronia, ma lorosono quelli che lo stanno facendo. Attendere ti consente di approfittare di quell'asincronia.

Inoltre, penso che ti manchi una terza opzione. Noi anziani - oggi i bambini con la loro musica rap dovrebbero scendere dal mio prato, ecc. - ricordiamo il mondo di Windows nei primi anni '90. Non c'erano macchine multi-CPU e nessuno scheduler di thread. Volevi eseguire due app di Windows contemporaneamente, dovevi cedere . Il multitasking era cooperativo . Il sistema operativo indica a un processo che deve essere eseguito e, se non funziona correttamente, fa morire di fame tutti gli altri processi. Funziona fino a quando non cede e, in qualche modo, deve sapere come riprendere da dove si era interrotto la prossima volta che il sistema operativo passa il controllo su di esso. Il codice asincrono a thread singolo è molto simile, con "wait" anziché "yield". Attendere significa "Ricorderò dove avevo lasciato qui, e lascerò correre qualcun altro per un po '; richiamami quando l'attività che sto aspettando è completa, e riprenderò da dove avevo interrotto". Penso che puoi vedere come questo rende le app più reattive, proprio come ha fatto nei giorni di Windows 3.

chiamare un metodo significa attendere il completamento del metodo

C'è la chiave che ti manca. Un metodo può tornare prima che il suo lavoro sia completo . Questa è l'essenza dell'asincronia proprio lì. Viene restituito un metodo, restituisce un'attività che significa "questo lavoro è in corso; dimmi cosa devo fare quando è completo". Il lavoro del metodo non è stato eseguito, anche se è tornato .

Prima che l'operatore attendesse, dovevi scrivere un codice che sembrava spaghetti infilati nel formaggio svizzero per affrontare il fatto che abbiamo del lavoro da fare dopo il completamento, ma con il ritorno e il completamento desincronizzati . Attendere ti consente di scrivere codice che assomigli al ritorno e al completamento siano sincronizzati, senza che siano effettivamente sincronizzati.


Anche altre lingue moderne di alto livello supportano comportamenti simili esplicitamente cooperativi (ovvero la funzione fa alcune cose, i rendimenti [possibilmente inviando un valore / oggetto al chiamante], continua da dove era stata interrotta quando il controllo viene restituito [possibilmente con input aggiuntivo fornito] ). I generatori sono molto grandi in Python, per prima cosa.
JAB

2
@JAB: assolutamente. I generatori sono chiamati "blocchi iteratori" in C # e usano la yieldparola chiave. Sia i asyncmetodi che gli iteratori in C # sono una forma di coroutine , che è il termine generale per una funzione che sa come sospendere l'operazione in corso per riprenderla in seguito. Oggigiorno un certo numero di lingue ha coroutine o flussi di controllo simili alla coroutine.
Eric Lippert,

1
L'analogia con la resa è buona: è il multitasking cooperativo all'interno di un processo. (e quindi evitando i problemi di stabilità del sistema di multitasking cooperativo a livello di sistema)
user253751

3
Penso che il concetto di "interruzioni della CPU" utilizzato per IO, non sia a conoscenza di molti "programmatori" modem, quindi pensano che un thread debba attendere ogni bit di IO.
Ian Ringrose,

Metodo @EricLippert Async di WebClient crea effettivamente thread aggiuntivo, vedi qui stackoverflow.com/questions/48366871/...
KevinBui

28

Sono davvero contento che qualcuno abbia posto questa domanda, perché per molto tempo ho anche creduto che i thread fossero necessari per la concorrenza. Quando ho visto per la prima volta i loop degli eventi , ho pensato che fossero una bugia. Ho pensato tra me e me "non c'è modo che questo codice possa essere simultaneo se funziona in un singolo thread". Tieni presente ciò dopo che avevo già attraversato la lotta per comprendere la differenza tra concorrenza e parallelismo.

Dopo una ricerca di mio, ho finalmente trovato il pezzo mancante: select(). In particolare, IO multiplexing, realizzato da vari kernel sotto nomi diversi: select(), poll(), epoll(), kqueue(). Si tratta di chiamate di sistema che, sebbene i dettagli di implementazione differiscano, consentono di passare un set di descrittori di file da guardare. Quindi è possibile effettuare un'altra chiamata che si blocca fino a quando non cambia uno dei descrittori di file guardati.

Pertanto, è possibile attendere un set di eventi IO (il ciclo di eventi principale), gestire il primo evento che viene completato e quindi restituire il controllo al ciclo di eventi. Risciacqua e ripeti.

Come funziona? Bene, la risposta breve è che è una magia a livello di hardware e kernel. Esistono molti componenti in un computer oltre alla CPU e questi componenti possono funzionare in parallelo. Il kernel può controllare questi dispositivi e comunicare direttamente con loro per ricevere determinati segnali.

Queste chiamate di sistema di multiplexing IO sono l'elemento fondamentale di loop di eventi a thread singolo come node.js o Tornado. Quando si esegue awaituna funzione, si sta osservando un determinato evento (il completamento di quella funzione), quindi si restituisce il controllo al loop dell'evento principale. Al termine dell'evento che stai guardando, la funzione (eventualmente) riprende da dove era stata interrotta. Le funzioni che consentono di sospendere e riprendere il calcolo in questo modo sono chiamate coroutine .


25

awaite asyncusa Task non Thread.

Il framework ha un pool di thread pronti per eseguire alcuni lavori sotto forma di oggetti Task ; presentare una Task ai mezzi di selezione della piscina un libero, già esistente 1 , filo per chiamare il metodo di azione compito.
La creazione di un task è questione di creare un nuovo oggetto, di gran lunga modo più veloce di creare un nuovo thread.

Dato un task è possibile fissare un Perfezionamento ad esso, è un nuovo Task oggetto da eseguire una volta che le estremità del filo.

Poiché async/awaitusano Task s non creano un nuovo thread.


Mentre la tecnica di programmazione degli interrupt è ampiamente utilizzata in tutti i sistemi operativi moderni, non credo che siano rilevanti qui.
È possibile avere due attività legate alla CPU in esecuzione in parallelo (interleaved effettivamente) in una singola CPU utilizzando aysnc/await.
Ciò non può essere spiegato semplicemente con il fatto che il sistema operativo supporta l'accodamento di IORP .


L'ultima volta che ho controllato i asyncmetodi trasformati dal compilatore in DFA , il lavoro è diviso in passaggi, ognuno terminato con awaitun'istruzione.
Il awaitinizia la sua attività e allegarlo una continuazione per eseguire il passo successivo.

Come esempio di concetto, ecco un esempio di pseudo-codice.
Le cose vengono semplificate per motivi di chiarezza e perché non ricordo esattamente tutti i dettagli.

method:
   instr1                  
   instr2
   await task1
   instr3
   instr4
   await task2
   instr5
   return value

Si trasforma in qualcosa del genere

int state = 0;

Task nextStep()
{
  switch (state)
  {
     case 0:
        instr1;
        instr2;
        state = 1;

        task1.addContinuation(nextStep());
        task1.start();

        return task1;

     case 1:
        instr3;
        instr4;
        state = 2;

        task2.addContinuation(nextStep());
        task2.start();

        return task2;

     case 2:
        instr5;
        state = 0;

        task3 = new Task();
        task3.setResult(value);
        task3.setCompleted();

        return task3;
   }
}

method:
   nextStep();

1 In realtà un pool può avere la sua politica di creazione di attività.


16

Non ho intenzione di competere con Eric Lippert o Lasse V. Karlsen, e altri, vorrei solo attirare l'attenzione su un altro aspetto di questa domanda, che penso non sia stata esplicitamente menzionata.

L'uso awaitda solo non rende la tua app magicamente reattiva. Se qualunque cosa tu faccia nel metodo che stai aspettando dai blocchi di thread dell'interfaccia utente, bloccherà comunque l'interfaccia utente allo stesso modo della versione non attendibile .

Devi scrivere il tuo metodo attendibile in modo specifico in modo da generare un nuovo thread o utilizzare qualcosa come una porta di completamento (che restituirà l'esecuzione nel thread corrente e chiamerà qualcos'altro per la continuazione ogni volta che viene segnalata la porta di completamento). Ma questa parte è ben spiegata in altre risposte.


3
Non è in primo luogo una competizione; è una collaborazione!
Eric Lippert,

16

Ecco come vedo tutto questo, potrebbe non essere super tecnicamente accurato ma mi aiuta, almeno :).

Esistono sostanzialmente due tipi di elaborazione (calcolo) che avvengono su una macchina:

  • elaborazione che avviene sulla CPU
  • elaborazione che avviene su altri processori (GPU, scheda di rete, ecc.), chiamiamoli IO.

Quindi, quando scriviamo un pezzo di codice sorgente, dopo la compilazione, a seconda dell'oggetto che utilizziamo (e questo è molto importante), l'elaborazione sarà legata alla CPU o all'IO , e in effetti può essere associata a una combinazione di tutti e due.

Qualche esempio:

  • se uso il metodo Write FileStreamdell'oggetto (che è un flusso), l'elaborazione sarà dire 1% CPU associato e 99% IO associato.
  • se uso il metodo Write NetworkStreamdell'oggetto (che è un flusso), l'elaborazione sarà dire 1% CPU associato e 99% IO associato.
  • se uso il metodo Write Memorystreamdell'oggetto (che è uno Stream), l'elaborazione sarà legata al 100% alla CPU.

Quindi, come vedi, dal punto di vista di un programmatore orientato agli oggetti, sebbene io acceda sempre a un Streamoggetto, ciò che accade sotto può dipendere fortemente dal tipo ultimo dell'oggetto.

Ora, per ottimizzare le cose, a volte è utile poter eseguire il codice in parallelo (nota che non uso la parola asincrona) se è possibile e / o necessario.

Qualche esempio:

  • In un'app desktop, voglio stampare un documento, ma non voglio aspettare.
  • Il mio server web server molti client allo stesso tempo, ognuno con le sue pagine in parallelo (non serializzate).

Prima di asincronizzare / aspettiamo, avevamo essenzialmente due soluzioni a questo:

  • Le discussioni . Era relativamente facile da usare, con le classi Thread e ThreadPool. I thread sono associati esclusivamente alla CPU .
  • Il "vecchio" modello di programmazione asincrona Begin / End / AsyncCallback . È solo un modello, non ti dice se sarai collegato alla CPU o all'IO. Se dai un'occhiata alle classi Socket o FileStream, è associato a IO, il che è interessante, ma lo usiamo raramente.

Async / await è solo un modello di programmazione comune, basato sul concetto di Task . È un po 'più facile da usare rispetto ai thread o ai pool di thread per le attività associate alla CPU e molto più semplice rispetto al vecchio modello Begin / End. Undercovers, tuttavia, è "solo" un wrapper ricco di funzionalità super sofisticato su entrambi.

Quindi, la vera vittoria è soprattutto nelle attività legate a IO , attività che non usano la CPU, ma async / wait è ancora solo un modello di programmazione, non ti aiuta a determinare come / dove verrà eseguita l'elaborazione alla fine.

Significa che non è perché una classe ha un metodo "DoSomethingAsync" che restituisce un oggetto Task che puoi presumere che sarà associato alla CPU (il che significa che forse è abbastanza inutile , specialmente se non ha un parametro token di annullamento) o IO Bound (il che significa che è probabilmente un must ), o una combinazione di entrambi (poiché il modello è abbastanza virale, il legame e i potenziali benefici possono essere, alla fine, super miscelati e non così ovvi).

Quindi, tornando ai miei esempi, facendo le mie operazioni di scrittura usando asincrono / wait su MemoryStream rimarrà vincolato alla CPU (probabilmente non ne trarrò beneficio), anche se sicuramente ne trarrò beneficio con file e flussi di rete.


1
Questa è una buona risposta usando il theadpool per il lavoro associato alla cpu è povero nel senso che i thread TP dovrebbero essere usati per scaricare le operazioni di IO. Ovviamente il lavoro legato alla CPU dovrebbe essere bloccato con avvertimenti e nulla preclude l'uso di più thread.
davidcarr,

3

Riassumendo altre risposte:

Async / await viene creato principalmente per le attività associate a IO poiché, usandole, si può evitare di bloccare il thread chiamante. Il loro uso principale è con thread dell'interfaccia utente in cui non si desidera che il thread venga bloccato su un'operazione di associazione IO.

Async non crea il proprio thread. Il thread del metodo chiamante viene utilizzato per eseguire il metodo asincrono fino a quando non trova un valore attendibile. Lo stesso thread continua quindi a eseguire il resto del metodo chiamante oltre la chiamata del metodo asincrono. All'interno del metodo asincrono chiamato, dopo essere tornato dall'attendibile, la continuazione può essere eseguita su un thread dal pool di thread: l'unico posto in cui viene visualizzato un thread separato.


Buon riassunto, ma penso che dovrebbe rispondere ad altre 2 domande per dare un quadro completo: 1. Su quale thread viene eseguito il codice atteso? 2. Chi controlla / configura il pool di thread menzionato: lo sviluppatore o l'ambiente di runtime?
Stojke,

1. In questo caso, principalmente il codice atteso è un'operazione associata a IO che non utilizza thread della CPU. Se si desidera utilizzare wait per l'operazione associata alla CPU, potrebbe essere generata un'attività separata. 2. Il thread nel pool di thread è gestito dall'Utilità di pianificazione che fa parte del framework TPL.
vaibhav kumar,

2

Provo a spiegarlo dal basso. Forse qualcuno lo trova utile. Ero lì, l'ho fatto, l'ho reinventato, quando ho creato semplici giochi in DOS in Pascal (bei vecchi tempi ...)

Quindi ... In ogni applicazione guidata da eventi ha un loop di eventi all'interno che è qualcosa del genere:

while (getMessage(out message)) // pseudo-code
{
   dispatchMessage(message); // pseudo-code
}

Le cornici di solito ti nascondono questo dettaglio ma è lì. La funzione getMessage legge l'evento successivo dalla coda degli eventi o attende fino a quando si verifica un evento: spostamento del mouse, keydown, keyup, clic, ecc. E quindi dispatchMessage invia l'evento al gestore eventi appropriato. Quindi attende il prossimo evento e così via fino a quando arriva un evento di chiusura che esce dai loop e termina l'applicazione.

I gestori di eventi dovrebbero funzionare rapidamente in modo che il ciclo di eventi possa eseguire il polling per altri eventi e l'interfaccia utente rimane reattiva. Cosa succede se un clic sul pulsante attiva un'operazione costosa come questa?

void expensiveOperation()
{
    for (int i = 0; i < 1000; i++)
    {
        Thread.Sleep(10);
    }
}

Bene, l'interfaccia utente non risponde fino a quando l'operazione di 10 secondi termina quando il controllo rimane all'interno della funzione. Per risolvere questo problema è necessario suddividere l'attività in piccole parti che possono essere eseguite rapidamente. Questo significa che non puoi gestire tutto in un singolo evento. È necessario eseguire una piccola parte del lavoro, quindi pubblicare un altro evento nella coda degli eventi per chiedere la continuazione.

Quindi cambieresti questo in:

void expensiveOperation()
{
    doIteration(0);
}

void doIteration(int i)
{
    if (i >= 1000) return;
    Thread.Sleep(10); // Do a piece of work.
    postFunctionCallMessage(() => {doIteration(i + 1);}); // Pseudo code. 
}

In questo caso viene eseguita solo la prima iterazione, quindi pubblica un messaggio nella coda degli eventi per eseguire l'iterazione successiva e restituisce. La nostra postFunctionCallMessagepseudo funzione di esempio mette un evento "chiama questa funzione" nella coda, quindi il dispatcher di eventi lo chiamerà quando lo raggiunge. Ciò consente a tutti gli altri eventi della GUI di essere elaborati mentre si eseguono continuamente pezzi di un lavoro di lunga durata.

Finché questa attività in esecuzione è in esecuzione, il suo evento di continuazione è sempre nella coda degli eventi. Quindi hai praticamente inventato il tuo programmatore di attività. Dove gli eventi di continuazione nella coda sono "processi" in esecuzione. In realtà questo è ciò che fanno i sistemi operativi, tranne per il fatto che l'invio degli eventi di continuazione e il ritorno al loop dello scheduler avviene tramite l'interruzione del timer della CPU in cui il sistema operativo ha registrato il codice di commutazione del contesto, quindi non è necessario preoccuparsene. Ma qui stai scrivendo il tuo programmatore, quindi devi preoccupartene - finora.

In questo modo possiamo eseguire attività a esecuzione prolungata in un singolo thread in parallelo con la GUI suddividendole in piccoli blocchi e inviando eventi di continuazione. Questa è l'idea generale della Taskclasse. Rappresenta un pezzo di lavoro e quando lo chiami .ContinueWith, definisci quale funzione chiamare come pezzo successivo al termine del pezzo corrente (e il suo valore di ritorno viene passato alla continuazione). La Taskclasse utilizza un pool di thread, in cui è presente un loop di eventi in ciascun thread in attesa di eseguire operazioni simili a quelle che desideravo mostrare all'inizio. In questo modo puoi avere milioni di attività in esecuzione in parallelo, ma solo pochi thread per eseguirle. Funzionerebbe altrettanto bene con un singolo thread, a condizione che le attività siano adeguatamente suddivise in piccoli pezzi, ognuno dei quali appare in esecuzione in parallelo.

Ma fare tutto questo concatenare suddividere il lavoro in piccoli pezzi manualmente è un lavoro ingombrante e .ContinueWithincasina totalmente il layout della logica, perché l'intero codice dell'attività in background è fondamentalmente un casino. Quindi è qui che il compilatore ti aiuta. Fa tutto questo incatenamento e continuazione per te in background. Quando dici awaitdi dire al compilatore che "ferma qui, aggiungi il resto della funzione come attività di continuazione". Il compilatore si occupa di tutto il resto, quindi non è necessario.


0

In realtà, le async awaitcatene sono macchine a stati generate dal compilatore CLR.

async await tuttavia utilizza i thread che TPL sta utilizzando il pool di thread per eseguire attività.

Il motivo per cui l'applicazione non è bloccata è che la macchina a stati può decidere quale co-routine eseguire, ripetere, controllare e decidere di nuovo.

Ulteriori letture:

Cosa genera asincrono e attendi?

Async Await e Generated StateMachine

C # e F # asincroni (III.): Come funziona? - Tomas Petricek

Modifica :

Va bene. Sembra che la mia elaborazione sia errata. Tuttavia, devo sottolineare che le macchine a stati sono risorse importanti per async awaits. Anche se si accetta l'I / O asincrono, è comunque necessario un supporto per verificare se l'operazione è stata completata, pertanto è ancora necessaria una macchina a stati e determinare quale routine può essere eseguita in modo asincrono insieme.


0

Questo non risponde direttamente alla domanda, ma penso che sia un'informazione aggiuntiva interessante:

Async e waitit non crea nuovi thread da solo. MA a seconda del punto in cui si utilizza async waitit, la parte sincrona PRIMA che l'attesa possa essere eseguita su un thread diverso rispetto alla parte sincrona DOPO l'attesa (ad esempio ASP.NET e ASP.NET core si comportano in modo diverso).

Nelle applicazioni basate su UI-Thread (WinForms, WPF) sarai sullo stesso thread prima e dopo. Ma quando usi asincronizzazione su un thread del pool di thread, il thread prima e dopo l'attesa potrebbe non essere lo stesso.

Un ottimo video su questo argomento

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.