Perché dovresti mai "aspettare" un metodo e quindi interrogare immediatamente il suo valore di ritorno?


24

In questo articolo MSDN , viene fornito il seguente codice di esempio (leggermente modificato per brevità):

public async Task<ActionResult> Details(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }

    Department department = await db.Departments.FindAsync(id);

    if (department == null)
    {
        return HttpNotFound();
    }

    return View(department);
}

Il FindAsyncmetodo recupera un Departmentoggetto in base al suo ID e restituisce a Task<Department>. Quindi il dipartimento viene immediatamente controllato per vedere se è nullo. A quanto ho capito, chiedere il valore dell'attività in questo modo bloccherà l'esecuzione del codice fino a quando non verrà restituito il valore dal metodo atteso, rendendolo effettivamente una chiamata sincrona.

Perché mai lo faresti? Non sarebbe più semplice chiamare il metodo sincrono Find(id), se hai intenzione di bloccare immediatamente comunque?


Potrebbe essere correlato all'implementazione. ... else return null;Quindi dovrai verificare che il metodo abbia effettivamente trovato il dipartimento che hai richiesto.
Jeremy Kato,

Non ne vedo nessuno in un asp.net, ma in un'app destop, facendolo in questo modo non stai congelando l'interfaccia utente
Rémi

Ecco un link che spiega il concetto di attesa del designer ... msdn.microsoft.com/en-us/magazine/hh456401.aspx
Jon Raynor,

Aspettare vale la pena considerare solo con ASP.NET se gli switch di contatto thread ti rallentano o se l'utilizzo della memoria da parte di molti stack di thread è un problema per te.
Ian,

Risposte:


24

A quanto ho capito, chiedere il valore dell'attività in questo modo bloccherà l'esecuzione del codice fino a quando non verrà restituito il valore dal metodo atteso, rendendolo effettivamente una chiamata sincrona.

Non proprio.

Quando si chiama await db.Departments.FindAsync(id)l'attività viene inviata e il thread corrente viene restituito al pool per essere utilizzato da altre operazioni. Il flusso di esecuzione è bloccato (come sarebbe indipendentemente dall'utilizzo departmentsubito dopo, se capisco le cose correttamente), ma il thread stesso è libero di essere utilizzato da altre cose mentre aspetti che l'operazione venga completata off-machine (e segnalato da un evento o da una porta di completamento).

Se hai chiamato, d.Departments.Find(id)il thread si trova lì e aspetta la risposta, anche se la maggior parte dell'elaborazione viene eseguita sul DB.

Stai effettivamente liberando risorse CPU quando associato al disco.


2
Ma pensavo che tutto ciò che awaitfacesse fosse firmare il resto del metodo come continuazione sullo stesso thread (ci sono eccezioni; alcuni metodi asincroni fanno girare il proprio thread) o firmare il asyncmetodo come continuazione sullo stesso thread e consentire codice rimanente da eseguire (come puoi vedere, non sono cristallino su come asyncfunziona). Quello che stai descrivendo sembra più una forma sofisticata diThread.Sleep(untilReturnValueAvailable)
Robert Harvey,

1
@RobertHarvey: lo assegna come continuazione, ma una volta che hai inviato l'attività (e la sua continuazione) per l'elaborazione, non c'è più niente da eseguire. Non è garantito che si trovi sullo stesso thread a meno che non lo specifichi (tramite ConfigureAwaitiirc).
Telastyn,

1
Vedi la mia risposta ... la continuazione è ricondotta al thread originale per impostazione predefinita.
Michael Brown,

2
Penso di vedere cosa mi sto perdendo qui. Affinché ciò fornisca qualsiasi vantaggio, il framework ASP.NET MVC deve essere awaitchiamato a public async Task<ActionResult> Details(int? id). In caso contrario, la chiamata originale verrà bloccata, in attesa di department == nullessere risolta.
Robert Harvey,

2
@RobertHarvey Quando await ..."ritorna" la FindAsyncchiamata è terminata. Questo è ciò che attende. Si chiama wait perché rende il tuo codice in attesa di cose. (Nota che non è lo stesso che far aspettare le cose dal thread corrente)
user253751

17

Odio davvero che nessuno degli esempi mostri come sia possibile attendere alcune righe prima di attendere l'attività. Considera questo.

Foo foo = await getFoo();
Bar bar = await getBar();

Console.WriteLine(“Do some other stuff to prepare.”);

doStuff(foo, bar);

Questo è il tipo di codice incoraggiato dagli esempi e hai ragione. C'è poco senso in questo. Libera il thread principale per fare altre cose, come rispondere all'input dell'interfaccia utente, ma il vero potere di asincronizzazione / attesa è che posso facilmente continuare a fare altre cose mentre sto aspettando il completamento di un'attività potenzialmente lunga. Il codice sopra "bloccherà" e attenderà di eseguire la linea di stampa fino a quando non avremo ottenuto Foo & Bar. Non c'è bisogno di aspettare però. Possiamo elaborarlo mentre aspettiamo.

Task<Foo> foo = getFoo();
Task<Bar> bar = getBar();

Console.WriteLine(“Do some other stuff to prepare.”);

doStuff(await foo, await bar);

Ora, con il codice riscritto, non ci fermiamo e aspettiamo i nostri valori fino a quando non è necessario. Sono sempre alla ricerca di questo tipo di opportunità. Essere intelligenti quando aspettiamo può portare a significativi miglioramenti delle prestazioni. Al giorno d'oggi abbiamo più core, potremmo anche usarli.


1
Hm, si tratta meno di core / thread e di più sull'utilizzo corretto delle chiamate asincrone.
Deduplicatore

Non sbagli @Deduplicator. Ecco perché ho citato "blocco" nella mia risposta. È difficile parlare di queste cose senza menzionare i thread, ma pur riconoscendo che potrebbe esserci o meno il multi-threading.
RubberDuck,

Ti suggerisco di stare attento con l'attesa all'interno di un'altra chiamata di metodo .. A volte il compilatore fraintende questo aspetto e ottieni un'eccezione davvero strana. Dopo che tutto asincrono / attende è solo zucchero di sintassi, puoi verificare con sharplab.io come appare il codice generato. L'ho osservato in diverse occasioni e ora aspetto solo una riga sopra la chiamata dove è necessario il risultato ... non ho bisogno di quei mal di testa.
evictednoise

"Non ha molto senso in questo." - C'è molto senso in questo. I casi in cui "continuare a fare altre cose" è applicabile con lo stesso metodo non sono così comuni; è molto più comune che tu voglia awaite lasciare che il thread faccia cose completamente diverse.
Sebastian Redl il

Quando ho detto "non ha molto senso in questo" @SebastianRedl, intendevo il caso in cui si potevano ovviamente eseguire entrambe le attività in parallelo anziché eseguire, attendere, eseguire, attendere. Questo è molto più comune di quanto sembri pensare. Scommetto che se ti guardassi intorno alla tua base di codice troverai opportunità.
RubberDuck

6

Quindi c'è altro che succede dietro le quinte qui. Async / Await è lo zucchero sintattico. Guarda prima la firma della funzione FindAsync. Restituisce un'attività. Stai già vedendo la magia della parola chiave, scompatta quell'attività in un dipartimento.

La funzione di chiamata non si blocca. Quello che succede è che l'assegnazione al dipartimento e tutto ciò che segue la parola chiave wait viene racchiuso in una chiusura e per tutti gli effetti passati al metodo Task.ContinueWith (la funzione FindAsync viene eseguita automaticamente su un thread diverso).

Naturalmente ci sono altre cose che accadono dietro le quinte perché l'operazione viene ricondotta al thread originale (quindi non devi più preoccuparti di sincronizzarti con l'IU quando esegui un'operazione in background) e nel caso in cui la funzione chiamante sia Async ( e viene chiamato in modo asincrono) la stessa cosa accade nello stack.

Quindi quello che succede è che ottieni la magia delle operazioni di Async, senza le insidie.


1

No, non torna immediatamente. L'attesa rende la chiamata del metodo asincrona. Quando viene chiamato FindAsync, il metodo Dettagli ritorna con un'attività che non è terminata. Al termine, FindAsync restituirà il risultato nella variabile di reparto e riprenderà il resto del metodo Dettagli.


No, non c'è alcun blocco. Non arriverà mai a department == null fino a quando il metodo asincrono non sarà già terminato.
Steve,

1
async awaitin genere non crea nuovi thread e, anche se lo fosse, è comunque necessario attendere il valore di quel reparto per scoprire se è nullo.
Robert Harvey,

2
Quindi, in pratica, ciò che stai dicendo è che, affinché questo possa essere di alcun beneficio, anche la chiamata a public async Task<ActionResult> deve essere modificata await.
Robert Harvey,

2
@RobertHarvey Sì, hai avuto l'idea. Async / Await è essenzialmente virale. Se "aspetti" una funzione, dovresti anche attendere tutte le funzioni che la chiamano. awaitnon deve essere miscelato con .Wait()o .Result, poiché ciò può causare deadlock. La catena asincrona / attende termina in genere in una funzione con una async voidfirma, utilizzata principalmente per i gestori di eventi o per le funzioni invocate direttamente dagli elementi dell'interfaccia utente.
KChaloux,

1
@Steve - " ... quindi sarebbe sul proprio thread. " No, leggi Le attività non sono ancora thread e asincrono non è parallelo . Ciò include l'output effettivo che dimostra che non è stato creato un nuovo thread.
ToolmakerSteve

1

Mi piace pensare "asincrono" come un contratto, un contratto che dice "posso eseguirlo in modo asincrono se ne hai bisogno, ma puoi chiamarmi come qualsiasi altra funzione sincrona".

Significa che uno sviluppatore stava prendendo delle funzioni e alcune decisioni di progettazione li hanno portati a prendere / contrassegnare un gruppo di funzioni come "asincrone". Il chiamante / consumatore delle funzioni è libero di usarle come decidono. Come dici tu puoi chiamare wait attendi prima della chiamata di funzione e attendere, in questo modo l'hai trattata come una funzione sincrona, ma se lo desideri, puoi chiamarla senza attendere come

Task<Department> deptTask = db.Departments.FindAsync(id);

e dopo, diciamo, 10 linee verso il basso della funzione chiamata

Department d = await deptTask;

quindi trattandolo come una funzione asincrona.

Tocca a voi.


1
È più come "Mi riservo il diritto di gestire il messaggio in modo asincrono, usalo per ottenere il risultato".
Deduplicatore

-1

"se hai intenzione di bloccare immediatamente comunque" la risposta è "Sì". Solo quando hai bisogno di una risposta veloce, wait / async ha senso. Ad esempio, il thread dell'interfaccia utente arriva a un metodo davvero asincrono, il thread dell'interfaccia utente tornerà e continuerà ad ascoltare il clic del pulsante, mentre il codice sotto "wait" verrà escluso da altri thread e infine otterrà il risultato.


1
Cosa fornisce questa risposta che le altre risposte no?
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.