web-dev-qa-db-fra.com

Différence entre wait et ContinueWith

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

attendre:

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

ContinueWith:

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érable à l'autre dans des situations particulières?

104
Harrison

Dans le second code, vous êtes synchrone en attente de la fin de la suite. Dans la première version, la méthode retournera à l'appelant dès qu'elle aura atteint la première expression await qui n'a pas encore été complété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, await conduit à beaucoup code plus simple. De plus, comme Servy l'a noté dans les commentaires, l'attente d'une tâche "décompresse" les exceptions agrégées, ce qui conduit généralement à une gestion des erreurs plus simple. Utiliser également await planifiera implicitement la suite dans le contexte de l'appel (sauf si vous utilisez ConfigureAwait). Ce n'est rien qui ne puisse être fait "manuellement", mais c'est beaucoup plus facile avec await.

Je vous suggère d’implémenter une séquence d’opérations légèrement plus longue avec await et Task.ContinueWith - cela peut être une véritable révélation.

88
Jon Skeet

Voici la séquence d'extraits de code que j'ai récemment utilisée pour illustrer la différence et que résout divers problèmes liés à l'utilisation asynchrone.

Supposons que votre application basée sur une interface graphique prenne un gestionnaire d'événements qui prend beaucoup de temps et que vous souhaitiez le rendre ainsi 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 renvoie une tâche qui produira éventuellement 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 vous renvoyez à partir de la méthode. Sinon, vous continuez à traiter d'autres éléments à partir de LoadNextItem.

Première idée pour la version asynchrone: il suffit d'utiliser des continuations! Et ignorons la partie en boucle pour le moment. Je veux dire, qu'est-ce qui pourrait éventuellement aller mal?

return LoadNextItem().ContinueWith(t => {
    string result = t.Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
    }
});

Génial, nous avons maintenant une méthode qui ne bloque pas! Il se bloque à la place. Toutes les mises à jour des contrôles de l'interface utilisateur doivent se produire sur le thread d'interface utilisateur. Vous devez donc en rendre compte. Heureusement, il existe une option pour spécifier la manière dont les continuations doivent être planifiées, et il existe une option par défaut pour cela:

return LoadNextItem().ContinueWith(t => {
    string result = t.Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
    }
},
TaskScheduler.FromCurrentSynchronizationContext());

Génial, nous avons maintenant une méthode qui ne plante pas! Il échoue silencieusement à la place. Les suites sont elles-mêmes des tâches distinctes, dont le statut n'est pas lié à celui de la tâche antécédente. Ainsi, même en cas d'erreur dans LoadNextItem, l'appelant ne verra qu'une tâche qui s'est terminée avec succès. D'accord, transmettez simplement l'exception, s'il en existe 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, cela fonctionne réellement. Pour un seul article. Maintenant, que diriez-vous de cette boucle. Il se trouve qu'une solution équivalente à la logique de la version synchrone d'origine se présentera comme suit:

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 gentil maintenant, n'est-ce pas?

86
pkt