L'ultimo attento può essere sostituito con un'attesa esplicita?
-
11-12-2019 - |
Domanda
Sto ancora imparando il async
/ await
, quindi per favore scusami se sto chiedendo qualcosa di ovvio.Considera il seguente esempio:
class Program {
static void Main(string[] args) {
var result = FooAsync().Result;
Console.WriteLine(result);
}
static async Task<int> FooAsync() {
var t1 = Method1Async();
var t2 = Method2Async();
var result1 = await t1;
var result2 = await t2;
return result1 + result2;
}
static Task<int> Method1Async() {
return Task.Run(
() => {
Thread.Sleep(1000);
return 11;
}
);
}
static Task<int> Method2Async() {
return Task.Run(
() => {
Thread.Sleep(1000);
return 22;
}
);
}
}
.
Questo si comporta come previsto e stampa "33" nella console.
Se sostituisco il secondo await
con un attesa esplicito ...
.
static async Task<int> FooAsync() {
var t1 = Method1Async();
var t2 = Method2Async();
var result1 = await t1;
var result2 = t2.Result;
return result1 + result2;
}
... Sembra avere lo stesso comportamento.
Questi due esempi sono completamente equivalenti?
E se sono equivalenti in questo caso, ci sono altri casi in cui la sostituzione dell'ultimo await
da un'aspetto esplicito farebbe una differenza?
Soluzione 3
Ok, penso di averlo capito così, quindi lascia che lo si spera, in ciò che si spera sia una spiegazione più completa delle risposte fornite finora ...
risposta breve
Sostituzione del secondo await
con un'attesa esplicita non avrà alcun effetto apprezzabile su un'applicazione console, ma bloccherà la filettatura UI di un'applicazione WPF o WinForms per la durata dell'attesa.
Inoltre, la gestione delle eccezioni è leggermente diversa (come notato da Stephen Cleary).
risposta lunga
In breve, il await
fa questo:
- .
- Se l'attività attesa è già finita, recupera il suo risultato e continua.
- Se non lo è, post La continuazione (il resto del metodo dopo il
await
) al corrente contesto di sincronizzazione, se ce n'è uno. Essenzialmente,await
sta cercando di restituzione di noi dove abbiamo iniziato da.- .
- Se non c'è un contesto corrente, utilizza semplicemente l'originale Taskscheduler , che è solitamente il pool di filo.
il secondo (e terzi e così via ...)
await
fa lo stesso.Poiché le applicazioni della console non hanno in genere alcun contesto di sincronizzazione, le continuazioni verranno normalmente gestite dal pool di filo, quindi non vi è alcun problema se blocciamo entro la continuazione.
Winforms o WPF, d'altra parte, avere un contesto di sincronizzazione implementato sulla parte superiore del loro ciclo di messaggi. Pertanto,
await
eseguita su un thread UI (alla fine) eseguirà anche la sua continuazione sulla filettatura dell'interfaccia utente. Se ci succediamo a bloccare la continuazione, bloccherà il cambio del messaggio e renderà l'interfaccia utente non reattiva finché non sblocciamo. OTOH, se soloawait
, pubblicherà ordinatamente le continuazioni da eseguire alla fine eseguita sulla filettatura dell'interfaccia utente, senza mai bloccare il filo UI.Nella seguente forma di Winforms, contenente un pulsante e un'etichetta, utilizzando
await
mantiene l'interfaccia utente reattivo in qualsiasi momento (nota ilasync
davanti al gestore clickler):
.public partial class Form1 : Form { public Form1() { InitializeComponent(); } private async void button1_Click(object sender, EventArgs e) { var result = await FooAsync(); label1.Text = result.ToString(); } static async Task<int> FooAsync() { var t1 = Method1Async(); var t2 = Method2Async(); var result1 = await t1; var result2 = await t2; return result1 + result2; } static Task<int> Method1Async() { return Task.Run( () => { Thread.Sleep(3000); return 11; } ); } static Task<int> Method2Async() { return Task.Run( () => { Thread.Sleep(5000); return 22; } ); } }
Se abbiamo sostituito il secondo
await
inFooAsync
cont2.Result
, continuerebbe a essere reattivi per circa 3 secondi dopo il pulsante clicca, quindi bloccare per circa 2 secondi:- .
- Il proseguimento dopo il primo
await
attenderà educatamente il suo turno di essere programmato sulla filettatura dell'interfaccia utente, che avverrà dopo che la commissioneMethod1Async()
finisce, I.e. Dopo circa 3 secondi, - A quale punto il
t2.Result
bloccherà brudamente la filettatura dell'interfaccia utente fino a quando l'attivitàMethod2Async()
finisce, circa 2 secondi dopo.Se abbiamo rimosso il
async
davanti albutton1_Click
e sostituito il suoawait
conFooAsync().Result
IT STAFLOCK:- .
- Il thread dell'interfaccia utente attenderebbe l'attività
FooAsync()
per terminare, - che aspetterebbe la sua continuazione alla finitura,
- che aspetterebbe il filo dell'interfaccia utente per diventare disponibile,
- che non è, poiché è bloccato dal
FooAsync().Result
.L'articolo "Attendi, sincronizzazioneContext e app console " di Stephen Toub è stato inestimabile per capire questo argomento.
- Il thread dell'interfaccia utente attenderebbe l'attività
- Il proseguimento dopo il primo
- Se non c'è un contesto corrente, utilizza semplicemente l'originale Taskscheduler , che è solitamente il pool di filo.
Altri suggerimenti
La versione sostitutiva blocca il thread chiamante in attesa dell'attività di finire.È difficile vedere una differenza visibile in un'app console come quella dal momento che stai bloccando intenzionalmente nel principale, ma non sono decisamente equivalenti.
non sono equivalenti.
Blocchi Task.Result
fino a quando il risultato è disponibile.Dato che spiego sul mio blog, questo può causare deadlock Se si dispone di un contesto async
che richiede accesso esclusivo (ad esempio, un'app UI o ASP.NET).
Inoltre, Task.Result
avvolgerà tutte le eccezioni in AggregateException
, quindi la gestione degli errori è più difficile se si è sincronizzata.