Différence entre Wait et ContinueWith

119

Quelqu'un peut-il expliquer si awaitet ContinueWithsont synonymes ou non dans l'exemple suivant. J'essaye d'utiliser TPL pour la première fois et j'ai lu toute la documentation, mais je ne comprends pas la différence.

Attendez :

String webText = await getWebPage(uri);
await parseData(webText);

Continuer avec :

Task<String> webText = new Task<String>(() => getWebPage(uri));
Task continue = webText.ContinueWith((task) =>  parseData(task.Result));
webText.Start();
continue.Wait();

L'un est-il préféré à l'autre dans des situations particulières?

Harrison
la source
3
Si vous avez retiré l' Waitappel dans le second exemple , puis les deux extraits seraient équivalents ( la plupart du temps).
Servy
2
duplication possible du mot-clé Attendre Async équivalent à un lambda ContinueWith?
Stephen Cleary
FYI: Votre getWebPageméthode ne peut pas être utilisée dans les deux codes. Dans le premier code, il a un Task<string>type de retour tandis que dans le second, il a un stringtype de retour. donc fondamentalement votre code ne se compile pas. - si pour être précis.
Royi Namir

Réponses:

101

Dans le deuxième code, vous attendez de manière synchrone que la poursuite se termine. Dans la première version, la méthode retournera à l'appelant dès qu'elle atteindra la première awaitexpression qui n'est pas déjà terminée.

Ils sont très similaires en ce sens qu'ils planifient tous les deux une continuation, mais dès que le flux de contrôle devient même légèrement complexe, cela awaitconduit à un code beaucoup plus simple. De plus, comme indiqué par Servy dans les commentaires, l'attente d'une tâche "déballe" les exceptions agrégées, ce qui conduit généralement à une gestion des erreurs plus simple. Utiliser également awaitplanifiera implicitement la poursuite dans le contexte d'appel (sauf si vous utilisez ConfigureAwait). Ce n'est rien qui ne peut être fait "manuellement", mais c'est beaucoup plus facile de le faire avec await.

Je vous suggère d'essayer d'implémenter une séquence d'opérations légèrement plus grande avec les deux awaitet Task.ContinueWith- cela peut être une véritable révélation.

Jon Skeet
la source
2
La gestion des erreurs entre les deux extraits de code est également différente; il est généralement plus facile de travailler avec awaitplus ContinueWithà cet égard.
Servy
@Servy: Vrai, ajoutera quelque chose à ce sujet.
Jon Skeet
1
La planification est également assez différente, c'est-à-dire dans quel contexte parseDatas'exécute.
Stephen Cleary
Lorsque vous dites que l' utilisation de await programmera implicitement la poursuite dans le contexte d'appel , pouvez-vous expliquer les avantages de cela et ce qui se passe dans l'autre situation?
Harrison
4
@Harrison: Imaginez que vous écrivez une application WinForms - si vous écrivez une méthode asynchrone, par défaut tout le code de la méthode s'exécutera dans le thread d'interface utilisateur, car la continuation y sera planifiée. Si vous ne spécifiez pas où vous voulez que la continuation s'exécute, je ne sais pas quelle est la valeur par défaut, mais elle pourrait facilement finir par s'exécuter sur un thread de pool de threads ... à quel point vous ne pouvez pas accéder à l'interface utilisateur, etc. .
Jon Skeet
100

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?

pkt
la source
Merci, très belle explication
Elger Mensonides
Ceci est un excellent exemple
Royi Namir