web-dev-qa-db-fra.com

Confusion concernant les threads et si les méthodes asynchrones sont vraiment asynchrones en C #

Je lisais sur async/wait et quand Task.Yield Pourrait être utile et est tombé sur ce post. J'avais une question concernant ce qui suit dans ce post:

Lorsque vous utilisez async/wait, rien ne garantit que la méthode que vous appelez lorsque vous attendez FooAsync() s'exécute réellement de manière asynchrone. L'implémentation interne est libre de revenir en utilisant un chemin complètement synchrone.

C'est un peu flou pour moi probablement parce que la définition de asynchrone dans ma tête n'est pas alignée.

Dans mon esprit, puisque je fais principalement du développement d'interface utilisateur, le code asynchrone est un code qui ne s'exécute pas sur le thread d'interface utilisateur, mais sur un autre thread. Je suppose que dans le texte que j'ai cité, une méthode n'est pas vraiment asynchrone si elle bloque sur n'importe quel thread (même s'il s'agit d'un thread de pool de threads par exemple).

Question:

Si j'ai une longue tâche en cours qui est liée au processeur (disons qu'elle fait beaucoup de calculs), alors l'exécution de cette tâche de manière asynchrone doit bloquer un certain thread, n'est-ce pas? Quelque chose doit réellement faire le calcul. Si je l'attends, un thread est bloqué.

Qu'est-ce qu'un exemple d'une méthode véritablement asynchrone et comment fonctionneraient-elles réellement? Ces opérations sont-elles limitées aux opérations d'E/S qui tirent parti de certaines capacités matérielles afin qu'aucun thread ne soit jamais bloqué?

48
Flack

C'est un peu flou pour moi probablement parce que la définition de asynchrone dans ma tête n'est pas alignée.

Je vous remercie d'avoir demandé des éclaircissements.

Dans mon esprit, puisque je fais principalement du développement d'interface utilisateur, le code asynchrone est un code qui ne s'exécute pas sur le thread d'interface utilisateur, mais sur un autre thread.

Cette croyance est courante mais fausse. Il n'est pas nécessaire que le code asynchrone s'exécute sur un deuxième thread.

Imaginez que vous préparez le petit-déjeuner. Vous mettez du pain grillé dans le grille-pain, et pendant que vous attendez que le pain grillé éclate, vous parcourez votre courrier d'hier, payez quelques factures, et bon, le pain grillé est apparu. Vous finissez de payer cette facture, puis vous beurrez votre toast.

Où avez-vous engagé un deuxième travailleur pour surveiller votre grille-pain?

Tu ne l'as pas fait. Les fils sont des travailleurs. Les workflows asynchrones peuvent se produire sur un seul thread. Le but du flux de travail asynchrone est d'éviter d'embaucher plus de travailleurs si vous pouvez éventuellement l'éviter.

Si j'ai une longue tâche en cours qui est liée au processeur (disons qu'elle fait beaucoup de calculs), alors l'exécution de cette tâche de manière asynchrone doit bloquer un certain thread, n'est-ce pas? Quelque chose doit réellement faire le calcul.

Ici, je vais vous donner un problème difficile à résoudre. Voici une colonne de 100 chiffres; veuillez les ajouter à la main. Vous ajoutez donc le premier au second et faites un total. Ensuite, vous ajoutez le total cumulé au troisième et obtenez un total. Oh, bon sang, la deuxième page de chiffres manque. Rappelez-vous où vous étiez et allez porter un toast. Oh, alors que le toast était en train de griller, une lettre est arrivée avec les chiffres restants. Lorsque vous avez fini de beurrer le pain grillé, continuez à additionner ces chiffres et n'oubliez pas de manger le pain grillé la prochaine fois que vous aurez un moment libre.

Où est la partie où vous avez embauché un autre travailleur pour ajouter les chiffres? n travail coûteux en calcul n'a pas besoin d'être synchrone et ne doit pas bloquer un thread. La chose qui rend le travail de calcul potentiellement asynchrone est la possibilité de l'arrêter, de se rappeler où vous étiez, d'aller faire autre chose, de vous rappeler quoi faire après cela , et reprenez là où vous vous étiez arrêté.

Maintenant, il est certainement possible d'embaucher un deuxième travailleur qui ne fait que rajouter des chiffres, puis est licencié. Et vous pourriez demander à ce travailleur "vous avez terminé?" et si la réponse est non, vous pourriez faire un sandwich jusqu'à ce qu'ils soient cuits. De cette façon, vous et le travailleur êtes occupés. Mais il n'y a pas d'exigence que l'asynchronie implique plusieurs travailleurs.

Si je l'attends, un thread est bloqué.

NON NON NON. C'est la partie la plus importante de votre malentendu. await ne signifie pas "lancez ce travail de manière asynchrone". await signifie "J'ai ici un résultat produit de manière asynchrone qui pourrait ne pas être disponible. S'il n'est pas disponible, trouver un autre travail à faire sur ce fil pour que nous bloquions le thread pas. Attendre est le ci-contre de ce que vous venez de dire.

Qu'est-ce qu'un exemple d'une méthode véritablement asynchrone et comment fonctionneraient-elles réellement? Ces opérations sont-elles limitées aux opérations d'E/S qui tirent parti de certaines capacités matérielles afin qu'aucun thread ne soit jamais bloqué?

Le travail asynchrone implique souvent du matériel personnalisé ou plusieurs threads, mais il n'est pas nécessaire.

Ne pensez pas aux travailleurs . Pensez aux workflows . L'essence de l'asynchronie est de diviser les workflows en petites parties de telle sorte que vous pouvez déterminer l'ordre dans lequel ces parties doivent arriver , puis exécuter chaque partie à son tour , mais autoriser les parties qui n'ont pas de dépendances entre elles à entrelacer .

Dans un workflow asynchrone, vous pouvez facilement détecter les endroits du workflow où une dépendance entre les pièces est exprimée . Ces pièces sont marquées par await. C'est le sens de await: le code qui suit dépend de la fin de cette partie du workflow, donc s'il n'est pas terminé, allez trouver une autre tâche à faire, et revenez ici plus tard lorsque la tâche sera terminée . Le but est de maintenir le travailleur au travail, même dans un monde où les résultats nécessaires seront produits à l'avenir.

109
Eric Lippert

Je lisais sur async/wait

Puis-je recommander mon async intro ?

et quand Task.Yield peut être utile

Presque jamais. Je le trouve parfois utile lors de tests unitaires.

Dans mon esprit, puisque je fais principalement du développement d'interface utilisateur, le code asynchrone est un code qui ne s'exécute pas sur le thread d'interface utilisateur, mais sur un autre thread.

Le code asynchrone peut être sans thread .

Je suppose que dans le texte que j'ai cité, une méthode n'est pas vraiment asynchrone si elle bloque sur n'importe quel thread (même s'il s'agit d'un thread de pool de threads par exemple).

Je dirais que c'est exact. J'utilise le terme "vraiment asynchrone" pour les opérations qui ne bloquent aucun thread (et qui ne sont pas synchrones). J'utilise également le terme "fausse async" pour les opérations qui apparaissent asynchrones mais fonctionnent uniquement de cette façon car elles s'exécutent ou bloquent un thread de pool de threads.

Si j'ai une longue tâche en cours qui est liée au processeur (disons qu'elle fait beaucoup de calculs), alors l'exécution de cette tâche de manière asynchrone doit bloquer un certain thread, n'est-ce pas? Quelque chose doit réellement faire le calcul.

Oui; dans ce cas, vous souhaitez définir ce travail avec une API synchrone (car il s'agit d'un travail synchrone), puis vous pouvez l'appeler à partir de votre thread d'interface utilisateur à l'aide de Task.Run, par exemple.:

var result = await Task.Run(() => MySynchronousCpuBoundCode());

Si je l'attends, un thread est bloqué.

Non; le thread du pool de threads serait utilisé pour exécuter le code (non réellement bloqué), et le thread d'interface utilisateur attend de manière asynchrone que ce code se termine (également non bloqué).

Qu'est-ce qu'un exemple d'une méthode véritablement asynchrone et comment fonctionneraient-elles réellement?

NetworkStream.WriteAsync (indirectement) demande à la carte réseau d'écrire quelques octets. Il n'y a aucun thread responsable d'écrire les octets un par un et d'attendre que chaque octet soit écrit. La carte réseau gère tout cela. Lorsque la carte réseau a terminé d'écrire tous les octets, elle termine (éventuellement) la tâche renvoyée par WriteAsync.

Ces opérations sont-elles limitées aux opérations d'E/S qui tirent parti de certaines capacités matérielles afin qu'aucun thread ne soit jamais bloqué?

Pas entièrement, bien que les opérations d'E/S soient des exemples faciles. Un autre exemple assez simple est celui des minuteries (par exemple, Task.Delay). Bien que vous puissiez créer une API vraiment asynchrone autour de tout type "d'événement".

26
Stephen Cleary

Lorsque vous utilisez async/wait, il n'y a aucune garantie que la méthode que vous appelez lorsque vous attendez FooAsync () s'exécute réellement de manière asynchrone. L'implémentation interne est libre de revenir en utilisant un chemin complètement synchrone.

C'est un peu flou pour moi probablement parce que la définition de asynchrone dans ma tête n'est pas alignée.

Cela signifie simplement qu'il existe deux cas lors de l'appel d'une méthode asynchrone.

La première est que, lors du retour de la tâche, l'opération est déjà terminée - ce serait un chemin synchrone. La seconde est que l'opération est toujours en cours - c'est le chemin asynchrone.

Considérez ce code, qui devrait montrer ces deux chemins. Si la clé se trouve dans un cache, elle est renvoyée de manière synchrone. Sinon, une opération asynchrone est lancée qui appelle une base de données:

Task<T> GetCachedDataAsync(string key)
{
    if(cache.TryGetvalue(key, out T value))
    {
        return Task.FromResult(value); // synchronous: no awaits here.
    }

    // start a fully async op.
    return GetDataImpl();

    async Task<T> GetDataImpl()
    {
        value = await database.GetValueAsync(key);
        cache[key] = value;
        return value;
    }
}

Donc, en comprenant cela, vous pouvez déduire qu'en théorie, l'appel de database.GetValueAsync() peut avoir un code similaire et lui-même être capable de retourner de manière synchrone: donc même votre chemin asynchrone peut finir par s'exécuter 100% de manière synchrone. Mais votre code n'a pas besoin de se soucier: async/expect gère les deux cas de manière transparente.

Si j'ai une longue tâche en cours qui est liée au processeur (disons qu'elle fait beaucoup de calculs), alors l'exécution de cette tâche de manière asynchrone doit bloquer un certain thread, n'est-ce pas? Quelque chose doit réellement faire le calcul. Si je l'attends, un thread est bloqué.

Le blocage est un terme bien défini - cela signifie que votre thread a cédé sa fenêtre d'exécution pendant qu'il attend quelque chose (E/S, mutex, etc.). Ainsi, votre thread effectuant le calcul n'est pas considéré comme bloqué: il effectue en fait un travail.

Qu'est-ce qu'un exemple d'une méthode véritablement asynchrone et comment fonctionneraient-elles réellement? Ces opérations sont-elles limitées aux opérations d'E/S qui tirent parti de certaines capacités matérielles afin qu'aucun thread ne soit jamais bloqué?

Une "méthode vraiment asynchrone" serait une méthode qui ne bloque tout simplement jamais. Cela implique généralement des E/S, mais cela peut également signifier awaiting votre code mathématique lourd lorsque vous voulez que votre thread actuel soit autre chose (comme dans le développement de l'interface utilisateur) ou lorsque vous essayez d'introduire le parallélisme:

async Task<double> DoSomethingAsync()
{
    double x = await ReadXFromFile();

    Task<double> a = LongMathCodeA(x);
    Task<double> b = LongMathCodeB(x);

    await Task.WhenAll(a, b);

    return a.Result + b.Result;
}
10
Cory Nelson

Asynchrone n'implique pas Parallèle

Asynchrone implique uniquement la simultanéité. En fait, même l'utilisation de threads explicites ne garantit pas qu'ils s'exécuteront simultanément (par exemple lorsque l'affinité des threads pour le même noyau unique, ou plus généralement lorsqu'il n'y a qu'un seul noyau dans la machine pour commencer).

Par conséquent, vous ne devez pas vous attendre à ce qu'une opération asynchrone se produise simultanément avec autre chose. Asynchrone signifie seulement que cela se produira, finalement à un autre moment ( a (grec) = sans, syn (grec ) = ensemble, khronos (grec) = temps. => Asynchrone = ne se produit pas en même temps).

Remarque: L'idée de l'asynchronicité est que lors de l'appel, vous ne vous souciez pas du moment où le code s'exécutera réellement. Cela permet au système de profiter du parallélisme, si possible, pour exécuter l'opération. Il peut même s'exécuter immédiatement. Cela pourrait même arriver sur le même thread ... plus à ce sujet plus tard.

Lorsque vous await l'opération asynchrone, vous créez simultané ( com (latin) = ensemble, currere (latin) = courir. => "Concurrent" = pour fonctionner ensemble). C'est parce que vous demandez que l'opération asynchrone soit terminée avant de continuer. On peut dire que l'exécution converge. Ceci est similaire au concept de jonction de threads.


Quand asynchrone ne peut pas être parallèle

Lorsque vous utilisez async/wait, il n'y a aucune garantie que la méthode que vous appelez lorsque vous attendez FooAsync () s'exécute réellement de manière asynchrone. L'implémentation interne est libre de revenir en utilisant un chemin complètement synchrone.

Cela peut se produire de trois manières:

  1. Il est possible d'utiliser await sur tout ce qui renvoie Task. Lorsque vous recevez le Task, il aurait déjà pu être terminé.

    Pourtant, cela seul ne signifie pas qu'il a fonctionné de manière synchrone. En fait, il suggère qu'il s'est exécuté de manière asynchrone et s'est terminé avant d'obtenir l'instance Task.

    Gardez à l'esprit que vous pouvez await sur une tâche déjà terminée:

    private static async Task CallFooAsync()
    {
        await FooAsync();
    }
    
    private static Task FooAsync()
    {
        return Task.CompletedTask;
    }
    
    private static void Main()
    {
        CallFooAsync().Wait();
    }
    

    De plus, si une méthode async n'a pas de await, elle s'exécutera de manière synchrone.

    Remarque: Comme vous le savez déjà, une méthode qui retourne un Task peut être en attente sur le réseau, ou sur le système de fichiers, etc… cela n'implique pas de démarrer un nouveau Thread ou une mise en file d'attente quelque chose sur le ThreadPool.

  2. Dans un contexte de synchronisation géré par un seul thread, le résultat sera d'exécuter le Task de manière synchrone, avec une surcharge. C'est le cas du thread d'interface utilisateur, je parlerai plus en détail de ce qui se passe ci-dessous.

  3. Il est possible d'écrire un TaskScheduler personnalisé pour toujours exécuter les tâches de manière synchrone. Sur le même thread, cela fait l'invocation.

    Remarque: récemment, j'ai écrit un SyncrhonizationContext personnalisé qui exécute les tâches sur un seul thread. Vous pouvez le trouver sur Création d'un (System.Threading.Tasks.) Planificateur de tâches . Il en résulterait un tel TaskScheduler avec un appel à FromCurrentSynchronizationContext .

    La valeur par défaut TaskScheduler mettra les appels en file d'attente dans ThreadPool . Pourtant, lorsque vous attendez l'opération, si elle ne s'est pas exécutée sur le ThreadPool, elle essaiera de la supprimer du ThreadPool et l'exécutera en ligne (sur le même thread qui attend ... le thread attend quand même, donc il n'est pas occupé).

    Remarque: Une exception notable est un Task marqué avec LongRunning . LongRunningTasks s'exécutera sur un thread séparé .


Ta question

Si j'ai une longue tâche en cours qui est liée au processeur (disons qu'elle fait beaucoup de calculs), alors l'exécution de cette tâche de manière asynchrone doit bloquer un certain thread, n'est-ce pas? Quelque chose doit réellement faire le calcul. Si je l'attends, un thread est bloqué.

Si vous faites des calculs, ils doivent se produire sur un thread, cette partie est juste.

Pourtant, la beauté de async et await est que le thread en attente n'a pas à être bloqué (plus à ce sujet plus tard). Pourtant, il est très facile de se tirer une balle dans le pied en programmant l'exécution de la tâche attendue sur le même thread en attente, ce qui entraîne une exécution synchrone (ce qui est une erreur facile dans le thread d'interface utilisateur).

L'une des principales caractéristiques de async et await est qu'ils prennent le SynchronizationContext de l'appelant. Pour la plupart des threads qui se traduit par l'utilisation du TaskScheduler par défaut (qui, comme mentionné précédemment, utilise le ThreasPool). Cependant, pour le thread d'interface utilisateur, cela signifie publier les tâches dans la file d'attente de messages, cela signifie qu'elles s'exécuteront sur le thread d'interface utilisateur. L'avantage de ceci est que vous n'avez pas à utiliser Invoke ou BeginInvoke pour accéder aux composants de l'interface utilisateur.

Avant d'entrer dans la façon de await a Task à partir du thread d'interface utilisateur sans le bloquer, je tiens à noter qu'il est possible d'implémenter un TaskScheduler où si vous await sur un Task, vous ne bloquez pas votre thread ou ne le laissez pas inactif, mais vous laissez votre thread choisir un autre Task en attente d'exécution. Quand j'étais tâches de rétroportage pour .NET 2.0 , j'ai expérimenté cela.

Qu'est-ce qu'un exemple d'une méthode véritablement asynchrone et comment fonctionneraient-elles réellement? Ces opérations sont-elles limitées aux opérations d'E/S qui tirent parti de certaines capacités matérielles afin qu'aucun thread ne soit jamais bloqué?

Vous semblez confondre asynchrone avec ne pas bloquer un thread. Si ce que vous voulez est un exemple d'opérations asynchrones dans .NET qui ne nécessitent pas de bloquer un thread, une façon de le faire que vous trouverez facile à comprendre est d'utiliser à la place continuations de await. Et pour les suites que vous devez exécuter sur le thread d'interface utilisateur, vous pouvez utiliser TaskScheduler.FromCurrentSynchronizationContext .

N'implémentez pas l'attente de spin fantaisie . Et j'entends par là utiliser un Timer , Application.Idle ou quelque chose comme ça.

Lorsque vous utilisez async, vous dites au compilateur de réécrire le code de la méthode d'une manière qui permet de le casser. Le résultat est similaire aux suites, avec une syntaxe beaucoup plus pratique. Lorsque le thread atteint un await, le Task sera planifié et le thread est libre de continuer après l'invocation actuelle de async (hors de la méthode). Lorsque le Task est terminé, la poursuite (après le await) est planifiée.

Pour le thread d'interface utilisateur, cela signifie qu'une fois qu'il atteint await, il est libre de continuer à traiter les messages. Une fois le Task attendu terminé, la poursuite (après le await) sera planifiée. Par conséquent, atteindre await n'implique pas de bloquer le thread.

Pourtant, l'ajout aveugle de async et await ne résoudra pas tous vos problèmes.

Je vous soumets une expérience. Obtenez une nouvelle application Windows Forms, déposez un Button et un TextBox , et ajoutez le code suivant:

    private async void button1_Click(object sender, EventArgs e)
    {
        await WorkAsync(5000);
        textBox1.Text = @"DONE";
    }

    private async Task WorkAsync(int milliseconds)
    {
        Thread.Sleep(milliseconds);
    }

Il bloque l'interface utilisateur. Ce qui se passe, c'est que, comme mentionné précédemment, await utilise automatiquement le SynchronizationContext du thread appelant. Dans ce cas, c'est le thread d'interface utilisateur. Par conséquent, WorkAsync s'exécutera sur le thread d'interface utilisateur.

Voici ce qui se passe:

  • Les threads de l'interface utilisateur reçoivent le message de clic et appellent le gestionnaire d'événements de clic
  • Dans le gestionnaire d'événements click, le thread d'interface utilisateur atteint await WorkAsync(5000)
  • WorkAsync(5000) (et planifiant sa poursuite) est planifié pour s'exécuter sur le contexte de synchronisation actuel, qui est le contexte de synchronisation du thread d'interface utilisateur… ce qui signifie qu'il publie un message pour l'exécuter
  • Le thread d'interface utilisateur est désormais libre de traiter d'autres messages
  • Le thread d'interface utilisateur sélectionne le message pour exécuter WorkAsync(5000) et planifier sa poursuite
  • Le thread d'interface utilisateur appelle WorkAsync(5000) avec continuation
  • Dans WorkAsync, le thread d'interface utilisateur exécute Thread.Sleep. L'interface utilisateur est maintenant irresponsable pendant 5 secondes.
  • La suite planifie l'exécution du reste du gestionnaire d'événements click, cela se fait en publiant un autre message pour le thread d'interface utilisateur
  • Le thread d'interface utilisateur est désormais libre de traiter d'autres messages
  • Le thread d'interface utilisateur sélectionne le message pour continuer dans le gestionnaire d'événements click
  • Le thread d'interface utilisateur met à jour la zone de texte

Le résultat est une exécution synchrone, avec surcharge.

Oui, vous devez utiliser Task.Delay à la place. Ce n'est pas la question; considérez Sleep comme un substitut pour certains calculs. Le fait est que l'utilisation de async et await partout ne vous donnera pas une application qui est automatiquement parallèle. Il est préférable de choisir ce que vous voulez exécuter sur un thread d'arrière-plan (par exemple sur le ThreadPool) et ce que vous voulez exécuter sur le thread d'interface utilisateur.

Maintenant, essayez le code suivant:

    private async void button1_Click(object sender, EventArgs e)
    {
        await Task.Run(() => Work(5000));
        textBox1.Text = @"DONE";
    }

    private void Work(int milliseconds)
    {
        Thread.Sleep(milliseconds);
    }

Vous constaterez que l'attente ne bloque pas l'interface utilisateur. C'est parce que dans ce cas Thread.Sleep fonctionne maintenant sur le ThreadPool grâce à Task.Run . Et grâce à button1_Click Étant async, une fois que le code atteint await, le thread d'interface utilisateur est libre de continuer à fonctionner. Une fois le Task terminé, le code reprendra après le await grâce au compilateur réécrivant la méthode pour permettre précisément cela.

Voici ce qui se passe:

  • Les threads de l'interface utilisateur reçoivent le message de clic et appellent le gestionnaire d'événements de clic
  • Dans le gestionnaire d'événements click, le thread d'interface utilisateur atteint await Task.Run(() => Work(5000))
  • Task.Run(() => Work(5000)) (et planifiant sa poursuite) est planifié pour s'exécuter sur le contexte de synchronisation actuel, qui est le contexte de synchronisation du thread d'interface utilisateur… ce qui signifie qu'il publie un message pour l'exécuter
  • Le thread d'interface utilisateur est désormais libre de traiter d'autres messages
  • Le thread d'interface utilisateur sélectionne le message pour exécuter Task.Run(() => Work(5000)) et planifier sa continuation une fois terminé
  • Le thread d'interface utilisateur appelle Task.Run(() => Work(5000)) avec continuation, cela s'exécutera sur le ThreadPool
  • Le thread d'interface utilisateur est désormais libre de traiter d'autres messages

Lorsque le ThreadPool se termine, la suite planifie l'exécution du reste du gestionnaire d'événements click, cela se fait en publiant un autre message pour le thread d'interface utilisateur. Lorsque le thread d'interface utilisateur sélectionne le message pour continuer dans le gestionnaire d'événements click, il met à jour la zone de texte.

6
Theraot

Ce sujet est assez vaste et plusieurs discussions peuvent survenir. Cependant, l'utilisation de async et await en C # est considérée comme une programmation asynchrone. Cependant, le fonctionnement de l'asynchronie est une discussion totalement différente. Jusqu'à .NET 4.5, il n'y avait pas de mots clés asynchrones et en attente, et les développeurs devaient développer directement contre Task Parallel Librar y (TPL). Là, le développeur avait un contrôle total sur quand et comment créer de nouvelles tâches et même des threads. Cependant, cela avait un inconvénient car n'étant pas vraiment un expert sur ce sujet, les applications pouvaient souffrir de problèmes de performances lourds et de bugs en raison des conditions de concurrence entre les threads, etc.

À partir de .NET 4.5, les mots clés async et wait ont été introduits, avec une nouvelle approche de la programmation asynchrone. Les mots clés asynchrones et en attente n'entraînent pas la création de threads supplémentaires. Les méthodes asynchrones ne nécessitent pas de multithreading car une méthode asynchrone ne s'exécute pas sur son propre thread. La méthode s'exécute sur le contexte de synchronisation actuel et utilise l'heure sur le thread uniquement lorsque la méthode est active. Vous pouvez utiliser Task.Run pour déplacer le travail lié au processeur vers un thread d'arrière-plan, mais un thread d'arrière-plan n'aide pas avec un processus qui attend simplement que les résultats soient disponibles.

L'approche asynchrone de la programmation asynchrone est préférable aux approches existantes dans presque tous les cas. En particulier, cette approche est meilleure que BackgroundWorker pour les opérations liées aux E/S car le code est plus simple et vous n'avez pas à vous prémunir contre les conditions de concurrence. Vous pouvez en savoir plus sur ce sujet ICI .

Je ne me considère pas comme une ceinture noire C # et certains développeurs plus expérimentés peuvent soulever d'autres discussions, mais en principe j'espère avoir réussi à répondre à votre question.

6
Dan

La réponse de Stephen est déjà excellente, donc je ne vais pas répéter ce qu'il a dit; J'ai fait ma juste part de répéter les mêmes arguments plusieurs fois sur Stack Overflow (et ailleurs).

Au lieu de cela, permettez-moi de me concentrer sur une chose abstraite importante concernant le code asynchrone: ce n'est pas un qualificatif absolu. Il est inutile de dire qu'un morceau de code est asynchrone - il est toujours asynchrone par rapport à autre chose . C'est assez important.

Le but de await est de créer des workflows synchrones en plus des opérations asynchrones et de certains codes synchrones de connexion. Votre code apparaît parfaitement synchrone1 au code lui-même.

var a = await A();
await B(a);

L'ordre des événements est spécifié par les invocations await. B utilise la valeur de retour de A, ce qui signifie que A doit avoir été exécuté avant B. La méthode contenant ce code a un flux de travail synchrone et les deux méthodes A et B sont synchrones l'une par rapport à l'autre.

Ceci est très utile, car les flux de travail synchrones sont généralement plus faciles à penser et, plus important encore, de nombreux flux de travail simplement sont synchrones. Si B a besoin du résultat de A pour s'exécuter, il doit s'exécuter après A2. Si vous devez effectuer une demande HTTP pour obtenir l'URL d'une autre demande HTTP, vous devez attendre la fin de la première demande; cela n'a rien à voir avec la planification des threads/tâches. Peut-être pourrions-nous appeler cette "synchronicité inhérente", à part "synchronicité accidentelle" où vous forcez l'ordre sur des choses qui n'ont pas besoin d'être ordonnées.

Vous dites:

Dans mon esprit, puisque je fais principalement du développement d'interface utilisateur, le code asynchrone est un code qui ne s'exécute pas sur le thread d'interface utilisateur, mais sur un autre thread.

Vous décrivez du code qui s'exécute de manière asynchrone par rapport à l'interface utilisateur. C'est certainement un cas très utile pour l'asynchronie (les gens n'aiment pas l'interface utilisateur qui cesse de répondre). Mais ce n'est qu'un cas spécifique d'un principe plus général - permettant aux choses de se produire dans le désordre les unes par rapport aux autres. Encore une fois, ce n'est pas un absolu - vous voulez que certains événements se produisent dans le désordre (par exemple, lorsque l'utilisateur fait glisser la fenêtre ou que la barre de progression change, le devrait toujours être redessinée), tandis que d'autres ne doivent pas se produire dans le désordre (le bouton Process ne doit pas être cliqué avant la fin de l'action Load). await dans ce cas d'utilisation n'est pas si différent de l'utilisation de Application.DoEvents en principe - il en introduit plusieurs problèmes et avantages.

C'est aussi la partie où la citation originale devient intéressante. L'interface utilisateur a besoin d'un thread pour être mis à jour. Ce thread appelle un gestionnaire d'événements, qui peut utiliser await. Cela signifie-t-il que la ligne où await est utilisée permettra à l'interface utilisateur de se mettre à jour en réponse aux entrées de l'utilisateur? Non.

Tout d'abord, vous devez comprendre que await utilise son argument, comme s'il s'agissait d'un appel de méthode. Dans mon exemple, A doit déjà avoir été invoqué avant que le code généré par await puisse faire quoi que ce soit, y compris "relâcher le contrôle dans la boucle d'interface utilisateur". La valeur de retour de A est Task<T> Au lieu de simplement T, représentant une "valeur possible dans le futur" - et await- vérifie le code généré pour voir si la valeur est déjà là (auquel cas elle continue simplement sur le même thread) ou non (ce qui signifie que nous pouvons relâcher le thread dans la boucle de l'interface utilisateur). Mais dans les deux cas, la valeur Task<T> Elle-même doit avoir été renvoyée par A.

Considérez cette implémentation:

public async Task<int> A()
{
  Thread.Sleep(1000);

  return 42;
}

L'appelant a besoin de A pour renvoyer une valeur (une tâche de int); puisqu'il n'y a pas de awaits dans la méthode, cela signifie le return 42;. Mais cela ne peut pas se produire avant la fin du sommeil, car les deux opérations sont synchrones par rapport au thread. Le thread appelant sera bloqué pendant une seconde, qu'il utilise ou non await - le blocage se fait dans A() lui-même, pas await theTaskResultOfA.

En revanche, considérez ceci:

public async Task<int> A()
{
  await Task.Delay(1000);

  return 42;
}

Dès que l'exécution arrive au await, il voit que la tâche en attente n'est pas encore terminée et renvoie le contrôle à son appelant; et le await dans l'appelant renvoie par conséquent le contrôle à son appelant. Nous avons réussi à rendre une partie du code asynchrone par rapport à l'interface utilisateur. La synchronicité entre le thread d'interface utilisateur et A était accidentelle, et nous l'avons supprimée.

La partie importante ici est: il n'y a aucun moyen de distinguer les deux implémentations de l'extérieur sans inspecter le code. Seul le type de retour fait partie de la signature de la méthode - il ne dit pas que la méthode s'exécutera de manière asynchrone, mais seulement qu'elle peut . Cela peut être pour un certain nombre de bonnes raisons, il n'y a donc aucun intérêt à le combattre - par exemple, il est inutile de rompre le fil d'exécution lorsque le résultat est déjà disponible:

var responseTask = GetAsync("http://www.google.com");

// Do some CPU intensive task
ComputeAllTheFuzz();

response = await responseTask;

Nous devons faire un peu de travail. Certains événements peuvent s'exécuter de manière asynchrone par rapport à d'autres (dans ce cas, ComputeAllTheFuzz est indépendant de la requête HTTP) et sont asynchrones. Mais à un moment donné, nous devons revenir à un flux de travail synchrone (par exemple, quelque chose qui nécessite à la fois le résultat de ComputeAllTheFuzz et la requête HTTP). C'est le point await, qui synchronise à nouveau l'exécution (si vous aviez plusieurs workflows asynchrones, vous utiliseriez quelque chose comme Task.WhenAll). Cependant, si la requête HTTP a réussi à se terminer avant le calcul, il n'y a aucun intérêt à libérer le contrôle au point await - nous pouvons simplement continuer sur le même thread. Il n'y a pas eu de gaspillage du CPU - pas de blocage du thread; il fait un travail CPU utile. Mais nous n'avons donné aucune possibilité à l'interface utilisateur de se mettre à jour.

C'est bien sûr pourquoi ce modèle est généralement évité dans les méthodes asynchrones plus générales. Il est utile pour certaines utilisations du code asynchrone (en évitant de gaspiller les threads et le temps CPU), mais pas pour d'autres (en gardant l'interface utilisateur réactive). Si vous vous attendez à ce qu'une telle méthode maintienne l'interface utilisateur réactive, vous ne serez pas satisfait du résultat. Mais si vous l'utilisez dans le cadre d'un service Web, par exemple, cela fonctionnera très bien - l'accent est mis sur le fait d'éviter de gaspiller les threads, de ne pas garder l'interface utilisateur réactive (cela est déjà fourni en appelant de manière asynchrone le point de terminaison du service - il n'y a aucun avantage à le faire encore la même chose du côté service).

En bref, await vous permet d'écrire du code asynchrone par rapport à son appelant. Il n'invoque pas un pouvoir magique d'asynchronicité, il n'est pas asynchrone par rapport à tout, il ne vous empêche pas d'utiliser le CPU ou de bloquer les threads. Il vous donne simplement les outils pour créer facilement un flux de travail synchrone à partir d'opérations asynchrones et présenter une partie de l'ensemble du flux de travail comme asynchrone par rapport à son appelant.

Prenons un gestionnaire d'événements d'interface utilisateur. Si les opérations asynchrones individuelles n'ont pas besoin d'un thread pour s'exécuter (par exemple, des E/S asynchrones), une partie de la méthode asynchrone peut permettre à d'autres codes de s'exécuter sur le thread d'origine (et l'interface utilisateur reste sensible dans ces parties). Lorsque l'opération nécessite à nouveau le CPU/thread, il peut ou non nécessiter le thread d'origine pour continuer le travail. Si c'est le cas, l'interface utilisateur sera à nouveau bloquée pendant la durée du travail du processeur; si ce n'est pas le cas (l'attente le spécifie à l'aide de ConfigureAwait(false)), le code de l'interface utilisateur s'exécutera en parallèle. En supposant qu'il y ait suffisamment de ressources pour gérer les deux, bien sûr. Si vous avez besoin de l'interface utilisateur pour rester réactif à tout moment, vous ne pouvez pas utiliser le thread d'interface utilisateur pour une exécution suffisamment longue pour être perceptible - même si cela signifie que vous devez encapsuler un asynchrone non fiable "généralement asynchrone, mais parfois bloqué pendant quelques secondes". dans un Task.Run. Il y a des coûts et des avantages dans les deux approches - c'est un compromis, comme pour toute ingénierie :)


  1. Bien sûr, parfait en ce qui concerne l'abstraction - chaque abstraction fuit, et il y a beaucoup de fuites en attente et d'autres approches de l'exécution asynchrone.
  2. Un optimiseur suffisamment intelligent pourrait permettre à une partie de B de s'exécuter, jusqu'au point où la valeur de retour de A est réellement nécessaire; c'est ce que fait votre CPU avec du code "synchrone" normal ( exécution hors service ). De telles optimisations doivent préserver l'apparence de la synchronicité, cependant - si le CPU évalue mal l'ordre des opérations, il doit rejeter les résultats et présenter un ordre correct.
4
Luaan

Voici un code asynchrone qui montre comment async/expect permet au code de bloquer et de libérer le contrôle sur un autre flux, puis de reprendre le contrôle sans avoir besoin d'un thread.

public static async Task<string> Foo()
{
    Console.WriteLine("In Foo");
    await Task.Yield();
    Console.WriteLine("I'm Back");
    return "Foo";
}


static void Main(string[] args)
{
    var t = new Task(async () =>
    {
        Console.WriteLine("Start");
        var f = Foo();
        Console.WriteLine("After Foo");        
        var r = await f;
        Console.WriteLine(r);
    });
    t.RunSynchronously();
    Console.ReadLine();
}

enter image description here

C'est donc cette libération du contrôle et la resynchronisation lorsque vous voulez des résultats qui sont essentiels avec async/wait (qui fonctionne bien avec le thread)

REMARQUE: aucun thread n'a été bloqué lors de la création de ce code :)

Je pense que parfois la confusion peut provenir de "Tâches" qui ne signifie pas que quelque chose tourne sur son propre thread. Cela signifie simplement une chose à faire, asynchroniser/attendre permet de diviser les tâches en étapes et de coordonner ces différentes étapes en un flux.

C'est un peu comme la cuisine, vous suivez la recette. Vous devez faire tout le travail de préparation avant d'assembler le plat pour la cuisson. Alors vous allumez le four, commencez à couper des choses, à râper des choses, etc. Ensuite, vous attendez la température du four et attendez le travail de préparation. Vous pouvez le faire vous-même en échangeant entre les tâches d'une manière qui semble logique (tâches/asynchrones/attendre), mais vous pouvez demander à quelqu'un d'autre d'aider à râper le fromage pendant que vous hachez des carottes (fils) pour accélérer les choses.

3
Keith Nicholas