Voici la séquence d'extraits de code que j'ai récemment utilisés pour illustrer la différence et divers problèmes liés à l'utilisation de l'async.
Supposons que votre application basée sur l'interface graphique comporte un gestionnaire d'événements qui prend beaucoup de temps et que vous souhaitiez le rendre asynchrone. Voici la logique synchrone avec laquelle vous commencez:
while (true) {
string result = LoadNextItem().Result;
if (result.Contains("target")) {
Counter.Value = result.Length;
break;
}
}
LoadNextItem retourne une tâche, qui finira par produire un résultat que vous souhaitez inspecter. Si le résultat actuel est celui que vous recherchez, vous mettez à jour la valeur d'un compteur sur l'interface utilisateur et revenez à partir de la méthode. Sinon, vous continuez à traiter plus d'éléments à partir de LoadNextItem.
Première idée pour la version asynchrone: il suffit d'utiliser les continuations! Et ignorons la partie en boucle pour le moment. Je veux dire, qu'est-ce qui pourrait mal tourner?
return LoadNextItem().ContinueWith(t => {
string result = t.Result;
if (result.Contains("target")) {
Counter.Value = result.Length;
}
});
Génial, maintenant nous avons une méthode qui ne bloque pas! Il plante à la place. Toutes les mises à jour des contrôles de l'interface utilisateur doivent se produire sur le thread de l'interface utilisateur, vous devrez donc en tenir compte. Heureusement, il existe une option pour spécifier comment les continuations doivent être planifiées, et il y en a une par défaut pour ceci:
return LoadNextItem().ContinueWith(t => {
string result = t.Result;
if (result.Contains("target")) {
Counter.Value = result.Length;
}
},
TaskScheduler.FromCurrentSynchronizationContext());
Génial, maintenant nous avons une méthode qui ne plante pas! Il échoue à la place en silence. Les suites sont elles-mêmes des tâches distinctes, leur statut n'étant pas lié à celui de la tâche précédente. Ainsi, même si LoadNextItem échoue, l'appelant ne verra qu'une tâche qui s'est terminée avec succès. D'accord, alors transmettez simplement l'exception, s'il y en a une:
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());
Génial, maintenant cela fonctionne réellement. Pour un seul article. Maintenant, que diriez-vous de cette boucle. Il s'avère qu'une solution équivalente à la logique de la version synchrone originale ressemblera à ceci:
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);
}});
}
Ou, au lieu de tout ce qui précède, vous pouvez utiliser async pour faire la même chose:
async Task AsyncLoop() {
while (true) {
string result = await LoadNextItem();
if (result.Contains("target")) {
Counter.Value = result.Length;
break;
}
}
}
C'est beaucoup plus agréable maintenant, n'est-ce pas?
Wait
appel dans le second exemple , puis les deux extraits seraient équivalents ( la plupart du temps).