web-dev-qa-db-fra.com

En quoi la fonctionnalité async-await de C # 5.0 diffère-t-elle du TPL?

Je ne vois pas la différence entre les nouvelles fonctionnalités asynchrones de C # (et de VB) et celles de .NET 4.0 Task Parallel Library . Prenons, par exemple, le code d'Eric Lippert à partir d'ici :

async void ArchiveDocuments(List<Url> urls) {
    Task archive = null;
    for(int i = 0; i < urls.Count; ++i) {
        var document = await FetchAsync(urls[i]);
        if (archive != null)
            await archive;
        archive = ArchiveAsync(document);
    }
}

Il semble que le mot-clé await a deux objectifs différents. La première occurrence (FetchAsync) semble signifier, "Si cette valeur est utilisée plus tard dans la méthode et sa tâche n'est pas terminée, attendez qu'elle se termine avant de continuer. " La deuxième instance (archive) semble signifier, " Si cette tâche n'est pas encore terminée, attendez maintenant jusqu'à ce qu'il se termine. " Si je me trompe, veuillez me corriger.

Ne pourrait-il pas être écrit aussi facilement comme ça?

void ArchiveDocuments(List<Url> urls) {
    for(int i = 0; i < urls.Count; ++i) {
        var document = FetchAsync(urls[i]);       // removed await
        if (archive != null)
            archive.Wait();                       // changed to .Wait()
        archive = ArchiveAsync(document.Result);  // added .Result
    }
}

J'ai remplacé le premier await par un Task.Result Où la valeur est réellement nécessaire, et le second await par Task.Wait(), où l'attente est en fait se produisant. La fonctionnalité est (1) Déjà implémentée, et (2) Beaucoup plus proche sémantiquement de ce qui se passe réellement dans le code.

Je me rends compte qu'une méthode async est réécrite comme une machine à états, similaire aux itérateurs, mais je ne vois pas non plus les avantages que cela apporte. Tout code qui nécessite un autre thread pour fonctionner (tel que le téléchargement) nécessitera toujours un autre thread, et tout code qui ne le fait pas (comme la lecture d'un fichier) pourrait toujours utiliser le TPL pour fonctionner avec un seul thread.

Il me manque évidemment quelque chose d'énorme ici; quelqu'un peut-il m'aider à comprendre un peu mieux cela?

61
zildjohn01

Je pense que le malentendu surgit ici:

Il semble que le mot-clé await a deux objectifs différents. La première occurrence (FetchAsync) semble signifier: "Si cette valeur est utilisée plus tard dans la méthode et que sa tâche n'est pas terminée, attendez qu'elle se termine avant de continuer." La deuxième instance (archive) semble signifier: "Si cette tâche n'est pas encore terminée, attendez maintenant qu'elle se termine." Si je me trompe, veuillez me corriger.

C'est en fait complètement incorrect. Les deux ont la même signification.

Dans votre premier cas:

var document = await FetchAsync(urls[i]);

Ce qui se passe ici, c'est que le moteur d'exécution dit "Commencez à appeler FetchAsync, puis renvoyez le point d'exécution actuel au thread appelant cette méthode." Il n'y a pas d '"attente" ici - à la place, l'exécution revient au contexte de synchronisation appelant et les choses continuent de tourner. À un moment donné dans le futur, la tâche de FetchAsync se terminera et à ce stade, ce code reprendra sur le contexte de synchronisation du thread appelant, et l'instruction suivante (affectant la variable de document) se produira.

L'exécution se poursuivra ensuite jusqu'au deuxième appel en attente - auquel moment, la même chose se produira - si le Task<T> (archive) n'est pas terminée, l'exécution sera libérée dans le contexte appelant - sinon, l'archive sera définie.

Dans le second cas, les choses sont très différentes - ici, vous bloquez explicitement, ce qui signifie que le contexte de synchronisation appelant n'aura jamais la possibilité d'exécuter du code tant que votre méthode ne sera pas terminée. Certes, il y a toujours asynchronie, mais l'asynchronie est complètement contenue dans ce bloc de code - aucun code en dehors de ce code collé ne se produira sur ce thread jusqu'à ce que tout votre code soit terminé.

71
Reed Copsey

Anders l'a résumé à une réponse très succincte dans l'interview de Channel 9 Live qu'il a faite. Je le recommande fortement

Les nouveaux mots clés Async et await vous permettent de orchestrer la concurrence dans vos applications. Ils n'introduisent en fait aucune concurrence dans votre application.

TPL et plus spécifiquement Task est one way que vous pouvez utiliser pour effectuer des opérations simultanément. Le nouveau mot-clé async et await vous permet de composer ces opérations simultanées de manière "synchrone" ou "linéaire".

Ainsi, vous pouvez toujours écrire un flux de contrôle linéaire dans vos programmes tandis que le calcul réel peut ou non se produire simultanément. Lorsque le calcul se produit simultanément, wait et async vous permettent de composer ces opérations.

24
Brad Cunningham

La capacité de transformer le flux de contrôle du programme en une machine à états est ce qui rend ces nouveaux mots-clés intéressants. Pensez-y comme donnant le contrôle , plutôt que comme des valeurs.

Regardez cette vidéo de Channel 9 d'Anders parlant de la nouvelle fonctionnalité.

6
John Leidegren

Le problème ici est que la signature de ArchiveDocuments est trompeuse. Il a un retour explicite de void mais en réalité le retour est Task. Pour moi, le vide implique synchrone car il n'y a aucun moyen "d'attendre" que cela se termine. Considérez la signature alternative de la fonction.

async Task ArchiveDocuments(List<Url> urls) { 
  ...
}

Pour moi, quand c'est écrit de cette façon, la différence est beaucoup plus évidente. La fonction ArchiveDocuments n'est pas une fonction qui se termine de manière synchrone mais se terminera plus tard.

4
JaredPar

L'appel à FetchAsync() restera bloqué jusqu'à ce qu'il se termine (sauf si une instruction dans les appels await?) La clé est que le contrôle est retourné à l'appelant (car la méthode ArchiveDocuments lui-même est déclaré comme async). Ainsi, l'appelant peut continuer à traiter la logique de l'interface utilisateur, répondre aux événements, etc.

Lorsque FetchAsync() se termine, il interrompt l'appelant pour terminer la boucle. Il frappe ArchiveAsync() et bloque, mais ArchiveAsync() crée probablement simplement une nouvelle tâche, la démarre et la renvoie. Cela permet à la deuxième boucle de commencer pendant le traitement de la tâche.

La deuxième boucle atteint FetchAsync() et se bloque, renvoyant le contrôle à l'appelant. Lorsque FetchAsync() se termine, il interrompt à nouveau l'appelant pour continuer le traitement. Il frappe alors await archive, Qui retourne le contrôle à l'appelant jusqu'à ce que le Task créé dans la boucle 1 se termine. Une fois cette tâche terminée, l'appelant est à nouveau interrompu et la deuxième boucle appelle ArchiveAsync(), qui obtient une tâche démarrée et commence la boucle 3, répétez ad nauseum.

La clé redonne le contrôle à l'appelant pendant que les poids lourds s'exécutent.

0
James King

Le mot-clé await n'introduit pas de concurrence. C'est comme le mot-clé yield, il dit au compilateur de restructurer votre code en lambda contrôlé par une machine à états.

Pour voir à quoi ressemblerait le code d'attente sans "attendre", consultez cet excellent lien: http://blogs.msdn.com/b/windowsappdev/archive/2012/04/24/diving-deep-with-winrt -et-await.aspx

0
Tb.