En bref: Async / Await Best Practices in .NET

En prévision du début du cours, "C # Developer" a préparé une traduction de matériel intéressant.




Async / Await - Introduction


La construction du langage Async / Await existe depuis C # version 5.0 (2012) et est rapidement devenue l'un des piliers de la programmation .NET moderne - tout développeur C # qui se respecte devrait l'utiliser pour améliorer les performances des applications, la réactivité globale et la lisibilité du code.

Async / Await rend l'introduction du code asynchrone d'une simplicité trompeuse et élimine la nécessité pour le programmeur de comprendre les détails de son traitement, mais combien d'entre nous savent vraiment comment cela fonctionne et quels sont les avantages et les inconvénients de cette méthode? Il y a beaucoup d'informations utiles, mais elles sont fragmentées, j'ai donc décidé d'écrire cet article.

Eh bien, approfondissons le sujet.

Machine d'Ă©tat (IAsyncStateMachine)


La première chose à savoir est que sous le capot, chaque fois que vous avez une méthode ou une fonction avec Async / Await, le compilateur transforme réellement votre méthode en une classe générée qui implémente l'interface IAsyncStateMachine. Cette classe est chargée de maintenir l'état de votre méthode pendant le cycle de vie d'une opération asynchrone - elle encapsule toutes les variables de votre méthode sous forme de champs et divise votre code en sections qui sont exécutées pendant les transitions de la machine d'état entre les états, afin que le thread puisse quitter la méthode et quand il reviendra, l'état ne changera pas.

À titre d'exemple, voici une définition de classe très simple avec deux méthodes asynchrones:

using System.Threading.Tasks;

using System.Diagnostics;

namespace AsyncAwait
{
    public class AsyncAwait
    {

        public async Task AsyncAwaitExample()
        {
            int myVariable = 0;

            await DummyAsyncMethod();
            Debug.WriteLine("Continuation - After First Await");
            myVariable = 1;

            await DummyAsyncMethod();
            Debug.WriteLine("Continuation - After Second Await");
            myVariable = 2;

        }

        public async Task DummyAsyncMethod()
        {
            // 
        }

    }
}

Une classe avec deux méthodes asynchrones

Si nous regardons le code généré pendant l'assemblage, nous verrons quelque chose comme ceci:



Notez que nous avons 2 nouvelles classes internes générées pour nous, une pour chaque méthode asynchrone. Ces classes contiennent une machine à états pour chacune de nos méthodes asynchrones.

De plus, après avoir étudié le code décompilé pour <AsyncAwaitExample> d__0, nous remarquerons que notre variable interne est «myVariable»maintenant un champ de classe:



nous pouvons également voir d'autres champs de classe utilisés en interne pour maintenir l'état IAsyncStateMachine. La machine d'état passe par des états en utilisant la méthodeMoveNext(), en fait, un gros interrupteur. Remarquez comment la méthode se poursuit dans différentes sections après chacun des appels asynchrones (avec l'étiquette de continuation précédente).



Cela signifie que l'élégance asynchrone / attente a un prix. L'utilisation de async / wait ajoute en fait une certaine complexité (que vous ne connaissez peut-être pas). Dans la logique côté serveur, cela peut ne pas être critique, mais en particulier lors de la programmation d'applications mobiles qui prennent en compte chaque cycle de mémoire CPU et Ko, vous devez garder cela à l'esprit, car la quantité de surcharge peut rapidement augmenter. Plus loin dans cet article, nous discuterons des meilleures pratiques pour utiliser Async / Await uniquement lorsque cela est nécessaire.

Pour une explication assez instructive de la machine d'état, regardez cette vidéo sur YouTube.

Quand utiliser Async / Await


Il existe généralement deux scénarios où Async / Await est la bonne solution.

  • Travail liĂ© aux E / S : votre code attendra quelque chose, comme des donnĂ©es d'une base de donnĂ©es, la lecture d'un fichier, l'appel d'un service Web. Dans ce cas, vous devez utiliser Async / Await, pas la bibliothèque parallèle de tâches.
  • Travail liĂ© au CPU : votre code effectuera des calculs complexes. Dans ce cas, vous devez utiliser Async / Await, mais vous devez commencer Ă  travailler dans un autre thread Ă  l'aide de Task.Run. Vous pouvez Ă©galement envisager d'utiliser la bibliothèque parallèle de tâches .



Async complètement


Lorsque vous commencez à travailler avec des méthodes asynchrones, vous remarquerez rapidement que la nature asynchrone du code commence à se propager de haut en bas dans votre hiérarchie d'appels - cela signifie que vous devez également rendre votre code appelant asynchrone, etc.
Vous pourriez être tenté d '«arrêter» cela en bloquant le code à l'aide de Task.Result ou Task.Wait, en convertissant une petite partie de l'application et en l'enveloppant dans une API synchrone afin que le reste de l'application soit isolé des modifications. Malheureusement, c'est une recette pour créer des blocages difficiles à suivre.

La meilleure solution à ce problème est de permettre au code asynchrone de se développer naturellement dans la base de code. Si vous suivez cette décision, vous verrez l'extension du code asynchrone à son point d'entrée, généralement un gestionnaire d'événements ou une action de contrôleur. Abandonnez-vous à l'asynchronie sans laisser de trace!

Plus d'informations dans cet article MSDN.

Si la méthode est déclarée asynchrone, assurez-vous qu'elle est en attente!


Comme nous l'avons vu, lorsque le compilateur trouve une méthode asynchrone, il transforme cette méthode en une machine à états. Si votre code n'a pas attendu dans son corps, le compilateur générera un avertissement, mais la machine d'état sera néanmoins créée, ajoutant une surcharge inutile pour une opération qui ne se terminera jamais réellement.

Évitez l'async void


Le vide asynchrone est quelque chose qui devrait vraiment être évité. Faites-en une règle pour utiliser la tâche asynchrone au lieu de l'async void.

public async void AsyncVoidMethod()
{
    //!
}

public async Task AsyncTaskMethod()
{
    //!
}

Les méthodes async void et async Task

Il y a plusieurs raisons Ă  cela, notamment:

  • Les exceptions levĂ©es dans la mĂ©thode async void ne peuvent pas ĂŞtre interceptĂ©es en dehors de cette mĂ©thode :
lorsqu'une exception est levée à partir de la méthode async Task ou async Task <T >, cette exception est interceptée et placée dans l'objet Task. Lorsque vous utilisez des méthodes async void, il n'y a pas d'objet Task, donc toutes les exceptions levées à partir de la méthode async void seront appelées directement dans SynchronizationContext, qui était actif lorsque la méthode async void a été exécutée.

Considérez l'exemple ci-dessous. Le bloc de capture ne sera jamais atteint.

public async void AsyncVoidMethodThrowsException()
{
    throw new Exception("Hmmm, something went wrong!");
}

public void ThisWillNotCatchTheException()
{
    try
    {
        AsyncVoidMethodThrowsException();
    }
    catch(Exception ex)
    {
        //     
        Debug.WriteLine(ex.Message);
    }
}

Les exceptions levées dans la méthode async void ne peuvent pas être interceptées en dehors de cette méthode.

Comparez avec ce code, oĂą au lieu de async void nous avons async Task. Dans ce cas, la capture sera accessible.

public async Task AsyncTaskMethodThrowsException()
{
    throw new Exception("Hmmm, something went wrong!");
}

public async Task ThisWillCatchTheException()
{
    try
    {
        await AsyncTaskMethodThrowsException();
    }
    catch (Exception ex)
    {
        //    
        Debug.WriteLine(ex.Message);
    }
}

L'exception est interceptée et placée dans l'objet Task.

  • Les mĂ©thodes asynchrones vides peuvent provoquer des effets secondaires indĂ©sirables si l'appelant ne s'attend pas Ă  ce qu'elles soient asynchrones : si votre mĂ©thode asynchrone ne renvoie rien, utilisez async Task (sans « <T >» pour Task) comme type de retour.
  • Les mĂ©thodes async void sont très difficiles Ă  tester : en raison des diffĂ©rences de gestion des erreurs et de disposition, il est difficile d'Ă©crire des tests unitaires qui appellent des mĂ©thodes async void. Test Asynchronous MSTest ne fonctionne que pour les mĂ©thodes asynchrones qui renvoient une tâche ou la tâche <T >.

Les gestionnaires d'événements asynchrones font exception à cette pratique. Mais même dans ce cas, il est recommandé de minimiser le code écrit dans le gestionnaire lui-même - attendez-vous à une méthode de tâche asynchrone qui contient la logique.

Plus d'informations dans cet article MSDN.

Préférer la tâche de retour au lieu du retour attendre


Comme déjà expliqué, chaque fois que vous déclarez une méthode asynchrone, le compilateur crée une classe de machine à états qui encapsule réellement la logique de votre méthode. Cela ajoute certains frais généraux qui peuvent s'accumuler, en particulier pour les appareils mobiles, où nous avons des limites de ressources plus strictes.

Parfois, une méthode n'a pas à être asynchrone, mais elle renvoie la tâche <T >et permet à l'autre côté de la gérer en conséquence. Si la dernière phrase de votre code est un retour d'attente, vous devez envisager de le refactoriser afin que le type de retour de la méthode soit la tâche <T>(au lieu de async T). Pour cette raison, vous évitez de générer une machine d'état, ce qui rend votre code plus flexible. Le seul cas où nous voulons vraiment attendre est lorsque nous faisons quelque chose avec le résultat de la tâche asynchrone dans la poursuite de la méthode.

public async Task<string> AsyncTask()

{
   //  !
   //...  -  
   //await -   ,  await  

   return await GetData();

}

public Task<string> JustTask()

{
   //!
   //...  -  
   // Task

   return GetData();

}

Préférer la tâche de retour plutôt que le retour d'attente

Notez que si nous n'avons pas d'attente et que nous renvoyons plutôt la tâche <T >, le retour se produit immédiatement, donc si le code est dans un bloc try / catch, l'exception ne sera pas interceptée. De même, si le code est à l'intérieur du bloc using, il supprimera immédiatement l'objet. Voir l'astuce suivante.

N'encapsulez pas la tâche de retour dans try..catch {} ou en utilisant des blocs {}


La tâche de retour peut provoquer un comportement non défini lorsqu'elle est utilisée dans un bloc try..catch (une exception levée par la méthode asynchrone ne sera jamais interceptée) ou dans un bloc using, car la tâche sera retournée immédiatement.

Si vous devez encapsuler votre code asynchrone dans un try..catch ou Ă  l'aide d'un bloc, utilisez plutĂ´t return attend.

public Task<string> ReturnTaskExceptionNotCaught()

{
   try
   {
       // ...

       return GetData();

   }
   catch (Exception ex)

   {
       //     

       Debug.WriteLine(ex.Message);
       throw;
   }

}

public Task<string> ReturnTaskUsingProblem()

{
   using (var resource = GetResource())
   {

       // ...  ,     , ,    

       return GetData(resource);
   }
}

N'encapsulez pas la tâche de retour dans des blocs try..catch{}ouusing{} .

Plus d'informations dans ce fil sur le débordement de pile.

Évitez d'utiliser .Wait()ou .Result- utilisez plutôtGetAwaiter().GetResult()


Si vous devez bloquer l' attente de la fin de la tâche asynchrone, utilisez GetAwaiter().GetResult(). Waitet Resultrenvoyez toutes les exceptions AggregateException, ce qui complique la gestion des erreurs. L'avantage GetAwaiter().GetResult()est qu'il renvoie à la place l'exception habituelle AggregateException.

public void GetAwaiterGetResultExample()

{
   // ,    ,     AggregateException  

   string data = GetData().Result;

   // ,   ,      

   data = GetData().GetAwaiter().GetResult();
}

Si vous devez bloquer l'attente de la fin de la tâche Async, utilisez le GetAwaiter().GetResult().

Plus d'informations sur ce lien .

Si la méthode est asynchrone, ajoutez le suffixe Async à son nom


Il s'agit de la convention utilisée dans .NET pour distinguer plus facilement les méthodes synchrones et asynchrones (à l'exception des gestionnaires d'événements ou des méthodes de contrôleur Web, mais elles ne doivent toujours pas être explicitement appelées par votre code).

Les méthodes de bibliothèque asynchrones doivent utiliser Task.ConfigureAwait (false) pour améliorer les performances



Le .NET Framework a le concept d'un «contexte de synchronisation», qui est un moyen de «revenir là où vous étiez auparavant». Chaque fois qu'une tâche est en attente, elle capture le contexte de synchronisation actuel avant d'attendre.

Une fois la tâche terminée .Post(), la méthode de contexte de synchronisation est appelée , ce qui reprend le travail à partir de son emplacement précédent. Ceci est utile pour revenir au thread d'interface utilisateur ou pour revenir au même contexte ASP.NET, etc.
Lorsque vous écrivez du code de bibliothèque, vous avez rarement besoin de revenir au contexte dans lequel vous vous trouviez auparavant. Lorsque Task.ConfigureAwait (false) est utilisé, le code n'essaie plus de reprendre d'où il était auparavant, mais, si possible, le code se termine dans le thread qui a terminé la tâche, ce qui évite le changement de contexte. Cela améliore légèrement les performances et peut aider à éviter les blocages.

public async Task ConfigureAwaitExample()

{
   //   ConfigureAwait(false)   .

   var data = await GetData().ConfigureAwait(false);
}

En règle générale, utilisez ConfigureAwait (false) pour les processus serveur et le code de bibliothèque.
Ceci est particulièrement important lorsque la méthode de bibliothèque est appelée un grand nombre de fois, pour une meilleure réactivité.

En règle générale, utilisez ConfigureAwait (false) pour les processus serveur en général. Peu nous importe quel thread est utilisé pour continuer, contrairement aux applications dans lesquelles nous devons revenir au thread de l'interface utilisateur.

Maintenant ... Dans ASP.NET Core, Microsoft a supprimé SynchronizationContext, donc théoriquement vous n'en avez pas besoin. Mais si vous écrivez du code de bibliothèque qui pourrait potentiellement être réutilisé dans d'autres applications (par exemple, UI App, Legacy ASP.NET, Xamarin Forms), cela reste la meilleure pratique .

Pour une bonne explication de ce concept, regardez cette vidéo .

Rapport d'avancement des tâches asynchrones


Un cas d'utilisation assez courant pour les méthodes asynchrones est de travailler en arrière-plan, de libérer le thread d'interface utilisateur pour d'autres tâches et de maintenir la réactivité. Dans ce scénario, vous souhaiterez peut-être signaler la progression à l'interface utilisateur afin que l'utilisateur puisse surveiller la progression du processus et interagir avec l'opération.

Pour résoudre ce problème courant, .NET fournit l'interface IProgress <T >, qui fournit la méthode Report <T >, qui est invoquée par une tâche asynchrone pour signaler la progression à l'appelant. Cette interface est acceptée comme paramètre de la méthode asynchrone - l'appelant doit fournir un objet qui implémente cette interface.

.NET fournit Progress <T >, l'implémentation par défaut d'IProgress <T >, qui est en fait recommandée, car il gère toute la logique de bas niveau associée à l'enregistrement et à la restauration du contexte de synchronisation. Progress <T >fournit également un événement Action <T et un rappel >- tous deux appelés lorsqu'une tâche signale la progression.

Ensemble, IProgress <T >et Progress <T >offrent un moyen simple de transférer les informations de progression d'une tâche d'arrière-plan vers un thread d'interface utilisateur.

Veuillez noter que <T>il peut s'agir d'une valeur simple, telle qu'un entier, ou d'un objet qui fournit des informations contextuelles sur la progression, telles que le pourcentage d'achèvement, une description de chaîne de l'opération en cours, ETA, etc.
Considérez la fréquence à laquelle vous signalez les progrès. Selon l'opération que vous effectuez, vous pouvez constater que vos rapports de code progressent plusieurs fois par seconde, ce qui peut rendre l'interface utilisateur moins réactive. Dans un tel scénario, il est recommandé de signaler les progrès à des intervalles plus longs.

Plus d'informations dans cet article sur le blog officiel Microsoft .NET.

Annuler les tâches asynchrones


Un autre cas d'utilisation courant pour les tâches en arrière-plan est la possibilité d'annuler l'exécution. .NET fournit la classe CancellationToken. La méthode asynchrone reçoit l'objet CancellationToken, qui est ensuite partagé par le code de l'appelant et la méthode asynchrone, fournissant ainsi un mécanisme pour signaler l'annulation.

Dans le cas le plus courant, l'annulation se produit comme suit:

  1. L'appelant crée un objet CancellationTokenSource.
  2. L'appelant appelle l'API asynchrone annulée et transmet le CancellationToken à partir du CancellationTokenSource (CancellationTokenSource.Token).
  3. L'appelant demande une annulation Ă  l'aide de l'objet CancellationTokenSource (CancellationTokenSource.Cancel ()).
  4. La tâche confirme l'annulation et s'annule elle-même, généralement à l'aide de la méthode CancellationToken.ThrowIfCancellationRequested.

Notez que pour que ce mécanisme fonctionne, vous devrez écrire du code pour vérifier les annulations demandées à intervalles réguliers (c'est-à-dire à chaque itération de votre code ou à un point d'arrêt naturel dans la logique). Idéalement, après une demande d'annulation, la tâche asynchrone doit être annulée le plus rapidement possible.

Vous devez envisager d'utiliser l'annulation pour toutes les méthodes qui peuvent prendre beaucoup de temps.

Plus d'informations dans cet article sur le blog officiel Microsoft .NET.

Rapport d'Ă©tape et d'annulation - Exemple


using System;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;
namespace TestAsyncAwait
{
   public partial class AsyncProgressCancelExampleForm : Form
   {
       public AsyncProgressCancelExampleForm()
       {
           InitializeComponent();
       }

       CancellationTokenSource _cts = new CancellationTokenSource();

       private async void btnRunAsync_Click(object sender, EventArgs e)

       {

           //   .

            <int>   ,          ,   ,    , ETA  . .

           var progressIndicator = new Progress<int>(ReportProgress);

           try

           {
               //   ,         

               await AsyncMethod(progressIndicator, _cts.Token);

           }

           catch (OperationCanceledException ex)

           {
               // 

               lblProgress.Text = "Cancelled";
           }
       }

       private void btnCancel_Click(object sender, EventArgs e)

       {
          // 
           _cts.Cancel();

       }

       private void ReportProgress(int value)

       {
           //    

           lblProgress.Text = value.ToString();

       }

       private async Task AsyncMethod(IProgress<int> progress, CancellationToken ct)

       {

           for (int i = 0; i < 100; i++)

           {
              //   ,     

               await Task.Delay(1000);

               //   

               if (ct != null)

               {

                   ct.ThrowIfCancellationRequested();

               }

               //   

               if (progress != null)

               {

                   progress.Report(i);
               }
           }
       }
   }
}

Attendre un certain temps


Si vous devez attendre un certain temps (par exemple, essayez à nouveau de vérifier la disponibilité de la ressource), assurez-vous d'utiliser Task.Delay - n'utilisez jamais Thread.Sleep dans ce scénario.

Attente de la fin de plusieurs tâches asynchrones


Utilisez Task.WaitAny pour attendre la fin d'une tâche. Utilisez Task.WaitAll pour attendre la fin de toutes les tâches.

Dois-je me précipiter pour passer en C # 7 ou 8? Inscrivez-vous à un webinaire gratuit pour discuter de ce sujet.

All Articles