Ecco la sequenza di frammenti di codice che ho usato di recente per illustrare la differenza e vari problemi utilizzando le soluzioni asincrone.
Supponi di avere un gestore di eventi nella tua applicazione basata su GUI che richiede molto tempo e quindi desideri renderlo asincrono. Ecco la logica sincrona con cui inizi:
while (true) {
string result = LoadNextItem().Result;
if (result.Contains("target")) {
Counter.Value = result.Length;
break;
}
}
LoadNextItem restituisce un'attività, che alla fine produrrà alcuni risultati che desideri esaminare. Se il risultato corrente è quello che stai cercando, aggiorni il valore di un contatore sull'interfaccia utente e torni dal metodo. In caso contrario, continui a elaborare più elementi da LoadNextItem.
Prima idea per la versione asincrona: usa solo le continuazioni! E ignoriamo la parte in loop per il momento. Voglio dire, cosa potrebbe andare storto?
return LoadNextItem().ContinueWith(t => {
string result = t.Result;
if (result.Contains("target")) {
Counter.Value = result.Length;
}
});
Ottimo, ora abbiamo un metodo che non blocca! Invece si blocca. Eventuali aggiornamenti ai controlli dell'interfaccia utente dovrebbero essere eseguiti nel thread dell'interfaccia utente, quindi sarà necessario tenerne conto. Per fortuna, c'è un'opzione per specificare come dovrebbero essere pianificate le continuazioni, e ce n'è una predefinita solo per questo:
return LoadNextItem().ContinueWith(t => {
string result = t.Result;
if (result.Contains("target")) {
Counter.Value = result.Length;
}
},
TaskScheduler.FromCurrentSynchronizationContext());
Ottimo, ora abbiamo un metodo che non si blocca! Invece fallisce silenziosamente. Le continuazioni sono esse stesse attività separate, con il loro stato non legato a quello dell'attività antecedente. Quindi, anche se LoadNextItem ha un errore, il chiamante vedrà solo un'attività che è stata completata con successo. Ok, quindi trasmetti l'eccezione, se ce n'è una:
return LoadNextItem().ContinueWith(t => {
if (t.Exception != null) {
throw t.Exception.InnerException;
}
string result = t.Result;
if (result.Contains("target")) {
Counter.Value = result.Length;
}
},
TaskScheduler.FromCurrentSynchronizationContext());
Fantastico, ora funziona davvero. Per un singolo articolo. Ora, che ne dici di quel loop. Si scopre che una soluzione equivalente alla logica della versione sincrona originale sarà simile a questa:
Task AsyncLoop() {
return AsyncLoopTask().ContinueWith(t =>
Counter.Value = t.Result,
TaskScheduler.FromCurrentSynchronizationContext());
}
Task<int> AsyncLoopTask() {
var tcs = new TaskCompletionSource<int>();
DoIteration(tcs);
return tcs.Task;
}
void DoIteration(TaskCompletionSource<int> tcs) {
LoadNextItem().ContinueWith(t => {
if (t.Exception != null) {
tcs.TrySetException(t.Exception.InnerException);
} else if (t.Result.Contains("target")) {
tcs.TrySetResult(t.Result.Length);
} else {
DoIteration(tcs);
}});
}
Oppure, invece di tutto quanto sopra, puoi usare async per fare la stessa cosa:
async Task AsyncLoop() {
while (true) {
string result = await LoadNextItem();
if (result.Contains("target")) {
Counter.Value = result.Length;
break;
}
}
}
Adesso è molto più carino, no?
Wait
chiamata nel secondo esempio poi i due frammenti sarebbero (in gran parte) equivalente.