Async/Await

Meilleures pratiques en matière de programmation asynchrone

Stephen Cleary

 

Aujourd'hui, les informations ne manquent pas sur la nouvelle prise en charge d'async et await dans le Microsoft .NET Framework 4.5. Cet article va plus loin dans l'apprentissage de la programmation asynchrone et je suppose que vous avez au moins lu un article d'introduction sur ce thème. Cet article ne présente rien de nouveau puisque vous trouverez les mêmes conseils en ligne dans des sources telles que Stack Overflow, les forums MSDN et le Forum Aux Questions consacré à async/await. Je présente simplement quelques meilleures pratiques qui pourraient se perdre au milieu de la multitude de documents disponibles.

Dans le cadre de cet article, ces pratiques sont plus des « recommandations » que de véritables règles, et chacune d'entre elles a des exceptions. J'expliquerai le raisonnement qui sous-tend chaque recommandation afin que vous compreniez clairement quand elle s'applique ou non. La figure 1 résume toutes ces recommandations que je détaillerai séparément dans les sections suivantes.

Figure 1 Résumé des recommandations en matière de programmation asynchrone

Nom Description Exceptions
Évitez async void Préférez les méthodes async Task à async void Gestionnaires d’événements
Async partout Ne mélangez pas code de blocage et code async Méthode principale de console
Configurez le contexte Utilisez ConfigureAwait(false) dès que possible Méthodes qui requièrent un contexte

Évitez async void

Il existe trois types de retour possible pour les méthodes asyn : Task, Task<T> et void, mais les types de retour naturels des méthodes async sont simplement Task et Task<T>. Lors de la conversion du code synchrone en code asynchrone, toute méthode qui retourne un type T devient une méthode async qui retourne Task<T>, et toute méthode qui retourne void devient une méthode async qui retourne Task. L'extrait de code suivant illustre une méthode synchrone qui retourne void et son équivalent asynchrone :

void MyMethod()
{
  // Do synchronous work.
  Thread.Sleep(1000);
}
async Task MyMethodAsync()
{
  // Do asynchronous work.
  await Task.Delay(1000);
}

Les méthodes async qui retournent void ont un objectif particulier : rendre possibles les gestionnaires d'événements asynchrones. Il est possible d'avoir un gestionnaire d'événements qui retourne un type réel donné mais qui ne fonctionne pas bien avec le langage, par exemple en appelant un gestionnaire d'événements qui retourne un type très étrange, et la notion d'un gestionnaire d'événements retournant réellement quelque chose n'a pas tellement de sens. Les gestionnaires d'événements retournent naturellement void, c'est pourquoi les méthodes async retournent void afin que vous puissiez avoir un gestionnaire d'événements asynchrones. Toutefois, une partie de la syntaxe d'une méthode async void diffère subtilement de celle d'une méthode async Task ou async Task<T>.

Les méthodes async void ont une syntaxe de gestion des erreurs différente. Lorsqu'une exception est générée d'une méthode async Task ou async Task<T>, elle est capturée et placée sur l'objet Task. Avec les méthodes async void, il n'y a aucun objet Task, c'est pourquoi toute exception générée par une méthode async void est levée directement sur le SynchronizationContext qui était actif lorsque la méthode async void a démarré. La figure 2 montre que les exceptions générées depuis des méthodes async void ne peuvent pas être interceptées naturellement.

Figure 2 Les exceptions depuis une méthode async void ne peuvent pas être interceptées avec Catch

private async void ThrowExceptionAsync()
{
  throw new InvalidOperationException();
}
public void AsyncVoidExceptions_CannotBeCaughtByCatch()
{
  try
  {
    ThrowExceptionAsync();
  }
  catch (Exception)
  {
    // The exception is never caught here!
    throw;
  }
}

Ces exceptions peuvent être observées avec AppDomain.UnhandledException ou un événement fourre-tout similaire pour les applications GUI/ASP.NET, mais l'utilisation de ces événements pour la gestion d'exceptions standard empêche toute maintenance.

Les méthodes async void ont une syntaxe de composition différente. Les méthodes async qui retournent Task ou Task<T> peuvent être composées facilement en utilisant await, Task.WhenAny, Task.WhenAll, etc. Les méthodes async qui retournent void ne permettent pas d'indiquer facilement au code appelant qu'elles ont terminé. Il est simple de démarrer plusieurs méthodes async void, mais il n'est pas facile de déterminer à quel moment elles se sont arrêtées. Les méthodes async void avertiront le SynchronizationContext de leur début et de leur fin, mais un SynchronizationContext personnalisé représente une solution complexe pour un code d'application standard.

Les méthodes async void sont difficiles à tester. En raison des différences dans la gestion des erreurs et la composition, il n'est pas simple d'écrire des tests unitaires qui appellent des méthodes async void. La prise en charge des tests asynchrones par MSTest ne fonctionne que pour les méthodes async qui retournent Task ou Task<T>. S'il est possible d'installer un SynchronizationContext qui détecte à quel moment toutes les méthodes async void ont terminé et collecte toutes les exceptions existantes, il est nettement plus simple de demander aux méthodes async void de retourner Task à la place.

Il est évident que les méthodes async void présentent plusieurs inconvénients par rapport aux méthodes async Task, mais elles sont particulièrement utiles dans un cas particulier : les gestionnaires d'événements asynchrones. Les différences de syntaxe sont justifiées pour les gestionnaires d'événements asynchrones. Elles lèvent les exceptions directement sur le SynchronizationContext, un comportement similaire à celui des gestionnaires d'événements synchrones. Les gestionnaires d'événements synchrones sont généralement privés et ne peuvent donc pas être composés ni testés directement. En règle générale, je préfère minimiser le code du gestionnaire d'événements asynchrones, par exemple en lui demandant d'attendre une méthode async Task qui contient la véritable logique. Le code suivant illustre cette approche en utilisant des méthodes async void pour les gestionnaires d'événements sans sacrifier pour autant les possibilités de tests :

private async void button1_Click(object sender, EventArgs e)
{
  await Button1ClickAsync();
}
public async Task Button1ClickAsync()
{
  // Do asynchronous work.
  await Task.Delay(1000);
}

Les méthodes async void peuvent faire des ravages si l'appelant ne s'attend pas à ce qu'elles soient asynchrones. Lorsque le type de retour est Task, l'appelant sait qu'il a affaire à une opération future. En revanche, lorsque le type de retour est void, l'appelant peut supposer que la méthode est terminée au moment du retour. Ce problème peut surgir de nombreuses façons inattendues. Il est généralement déconseillé de fournir l'implémentation asynchrone (ou le remplacement) d'une méthode qui retourne void sur une interface (ou une classe de base). Certains événements supposent également que leurs gestionnaires sont terminés au moment du retour. Faites notamment attention à un piège subtil si vous passez un lambda asynchrone à une méthode qui prend un paramètre Action. En effet, dans ce cas, le lambda asynchrone retourne void et hérite de tous les problèmes des méthodes async void. En règle générale, les lambdas async doivent uniquement être utilisés s'ils sont convertis en type de délégué qui retourne Task (par exemple, Func<Task>).

En résumé, cette première recommandation vous conseille de préférer async Task à async void. Les méthodes async Task facilitent la gestion des erreurs, la composabilité et les tests. Cette recommandation présente une exception, les gestionnaires d'événements asynchrones, qui doivent quant à eux retourner void. Cette exception comprend des méthodes qui sont logiquement des gestionnaires d'événements, même s'ils ne sont pas littéralement des gestionnaires d'événements (par exemple, les implémentations d'ICommand.Execute).

Async partout

Le code asynchrone me rappelle l'histoire d'un homme qui déclarait que le monde était suspendu dans l'espace. Une vieille femme le contredit immédiatement en déclarant que le monde se trouvait sur le dos d'une tortue géante. Lorsque l'homme lui demanda sur quoi se trouvait la tortue, la femme lui répondit : « Vous êtes très intelligent jeune homme, mais il n'y a que des tortues ! » Lorsque vous convertirez du code synchrone en code asynchrone, vous découvrirez que cela fonctionne mieux si le code asynchrone appelle et est appelé par un autre code asynchrone tout du long. D'autres personnes ont également remarqué le comportement de diffusion de la programmation asynchrone et l'ont qualifié de « contagieux » ou bien l'ont comparé à un virus zombie. Que l'on parle de tortues ou de zombies, il ne fait absolument aucun doute que le code asynchrone a tendance à entraîner l'asynchronie du code environnant. Ce comportement est inhérent à tous les types de programmation asynchrone, et pas uniquement aux nouveaux mots clés async/await.

« Asynchrone partout » signifie que vous ne devriez pas mélanger le code synchrone au code asynchrone à moins d'avoir évalué attentivement les conséquences d'une telle opération. Il est plus particulièrement déconseillé de bloquer du code asynchrone en appelant Task.Wait ou Task.Result. Ce problème est particulièrement courant chez les programmeurs qui s'aventurent timidement dans l'expérience de la programmation asynchrone, en convertissant uniquement une petite partie de leur application et en l'incluant dans une API synchrone afin que le reste de l'application soit isolé des modifications. Malheureusement, ils rencontrent alors des problèmes avec les blocages. Après avoir répondu à de nombreuses questions liées à l'asynchronie sur les forums MSDN, sur Stack Overflow et par courrier électronique, je peux dire que la question suivante est la plus posée, et de loin, par les débutants en asynchronie une fois qu'ils ont appris les bases : « Pourquoi mon code partiellement asynchrone se bloque-t-il ? »

La figure 3 illustre un exemple simple de blocage d'une méthode sur le résultat d'une méthode asynchrone. Ce code fonctionnera très bien sur une application de console, mais il se bloquera s'il est appelé depuis un contexte GUI ou ASP.NET. Ce comportement peut susciter de la confusion, notamment si l'on considère que l'analyse du débogueur révèle que c'est le mot clé await qui ne se termine jamais. La véritable cause du blocage se situe un peu plus haut dans la pile d'appels, lorsque Task.Wait est appelé.

Figure 3 Un problème de blocage courant lors du blocage sur un code asynchrone

public static class DeadlockDemo
{
  private static async Task DelayAsync()
  {
    await Task.Delay(1000);
  }
  // This method causes a deadlock when called in a GUI or ASP.NET context.
  public static void Test()
  {
    // Start the delay.
    var delayTask = DelayAsync();
    // Wait for the delay to complete.
    delayTask.Wait();
  }
}

La cause première de ce blocage est due à la façon dont await gère les contextes. Par défaut, lorsqu'une Task non terminée est attendue, le « contexte » en cours est capturé et utilisé pour reprendre la méthode une fois la Task terminée. Ce « contexte » est le SynchronizationContext sauf s'il est null, auquel cas il s'agit du TaskScheduler en cours. Les applications GUI et ASP.NET ont un SynchronizationContext qui autorise l'exécution d'un seul morceau de code à la fois. Une fois await terminé, il tente d'exécuter le reste de la méthode async dans le contexte capturé. Toutefois, ce contexte comporte déjà un thread, qui attend (de façon synchrone) la fin de la méthode async. Ils s'attendent mutuellement, ce qui entraîne un blocage.

Notez que les applications de console ne sont pas à l'origine de ce blocage. Elles ont un SynchronizationContext de pool de threads au lieu d'un SynchronizationContext pour un morceau à la fois. Par conséquent, une fois await terminé, il planifie le reste de la méthode async sur un thread de pool de threads. La méthode peut se terminer, et la tâche retournée est alors terminée aussi, sans aucun blocage. Cette différence de comportement peut être perturbante lorsque les programmeurs écrivent un programme de console de test, observent le code partiellement asynchrone comme prévu, puis déplacent le même code dans une application GUI ou ASP.NET, où se produit le blocage.

La meilleure solution pour résoudre ce problème consiste à permettre au code asynchrone de croître naturellement dans la base de code. Si vous optez pour cette solution, vous verrez que le code asynchrone s'étend jusqu'à son point d'entrée, généralement une action de gestionnaire d'événements ou de contrôleur. Les applications de console ne peuvent pas suivre totalement cette solution parce que la méthode Main ne peut pas être asynchrone. Si la méthode Main était asynchrone, elle pourrait être retournée avant d'être terminée, ce qui provoquerait la fin du programme. La figure 4 montre l'exception à cette recommandation : la méthode Main d'une application de console est l'une des rares situations où le code peut être bloqué sur une méthode asynchrone.

Figure 4 La méthode Main peut appeler Task.Wait ou Task.Result

class Program
{
  static void Main()
  {
    MainAsync().Wait();
  }
  static async Task MainAsync()
  {
    try
    {
      // Asynchronous implementation.
      await Task.Delay(1000);
    }
    catch (Exception ex)
    {
      // Handle exceptions.
    }
  }
}

La meilleure solution consiste à permettre à async de croître dans le code de base, mais cela implique beaucoup de travail initial avant qu'une application puisse bénéficier réellement du code asynchrone. Quelques techniques permettent de convertir de façon incrémentielle une base de code importante en code asynchrone, mais elles dépassent le cadre de cet article. Dans certains cas, l'utilisation de Task.Wait ou de Task.Result peut faciliter une conversion partielle, mais vous devez être conscient du problème de blocage, ainsi que de celui lié à la gestion des erreurs. Je vais maintenant expliquer le problème de gestion des erreurs, puis je détaillerai plus loin dans cet article la façon dont vous pouvez éviter le problème de blocage.

Chaque Task va stocker une liste d'exceptions. Lorsque vous attendez une Task, la première exception est à nouveau levée afin que vous puissiez intercepter le type d'exception, par exemple InvalidOperationException. Toutefois, lorsque vous bloquez une Task à l'aide de Task.Wait ou Task.Result, toutes les exceptions sont incluses dans une AggregateException et sont levées. Consultez à nouveau la figure 4. Le bloc try/catch de MainAsync interceptera un type d'exception spécifique, mais si vous placez try/catch dans Main, il interceptera systématiquement une AggregateException. La gestion des erreurs est bien plus facile à traiter si vous n'avez pas d'AggregateException, c'est pourquoi j'ai inséré le try/catch « global » dans MainAsync.

Jusqu'à présent, j'ai décrit deux problèmes de blocage sur le code asynchrone : les blocages possibles et une gestion des erreurs plus compliquée. Il existe également un problème avec l'utilisation du code de blocage au sein d'une méthode asynchrone. Prenons l'exemple simple suivant :

public static class NotFullyAsynchronousDemo
{
  // This method synchronously blocks a thread.
  public static async Task TestNotFullyAsync()
  {
    await Task.Yield();
    Thread.Sleep(5000);
  }
}

Cette méthode n'est pas totalement asynchrone. Elle s'arrêtera immédiatement et retournera une tâche non terminée, mais lorsqu'elle reprendra, elle bloquera de façon synchrone le thread en cours d'exécution. Si cette méthode est appelée depuis un contexte GUI, elle bloquera le thread de l'interface utilisateur. Si elle est appelée depuis un contexte de requête ASP.NET, elle bloquera le thread de requête ASP.NET en cours. Le code asynchrone fonctionne mieux s'il ne se bloque pas de façon synchrone. La figure 5 résume les solutions de remplacement asynchrones pour des opérations synchrones.

Figure 5 Opérations à la « mode asynchrone »

Pour effectuer cette opération… Au lieu de… Utilisez
Extraire le résultat d'une tâche en arrière-plan Task.Wait ou Task.Result await
Attendre la fin d'une tâche Task.WaitAny await Task.WhenAny
Extraire les résultats de plusieurs tâches Task.WaitAll await Task.WhenAll
Attendre un certain temps Thread.Sleep await Task.Delay

Pour résumer cette deuxième recommandation, évitez de mélanger du code asynchrone et du code de blocage. Cela risque en effet de provoquer des blocages, de rendre plus complexe la gestion des erreurs et d'entraîner un blocage inattendu des threads de contexte. Cette recommandation a une exception, la méthode Main pour les applications de contexte ou, si vous êtes un utilisateur avancé, la gestion d'une base de code partiellement asynchrone.

Configurez le contexte

Précédemment dans cet article, j'ai brièvement expliqué comment le « contexte » est capturé par défaut lorsqu'une Task non terminée est attendue et comment ce contexte capturé est utilisé pour résumer la méthode async. L'exemple de la figure 3 illustre comment la reprise dans ce contexte s'oppose au blocage synchrone et provoque un blocage. Ce comportement de contexte peut également entraîner un autre problème lié aux performances. À mesure que les applications GUI asynchrones deviennent plus importantes, vous découvrirez peut-être de nombreuses petites parties de méthodes async qui utilisent toutes le thread d'interface utilisateur comme contexte. Cela peut entraîner une certaine lenteur dans la mesure où la réactivité souffre de « milliers d'interventions ».

Pour atténuer ce problème, attendez le résultat de ConfigureAwait chaque fois que vous le pouvez. L'extrait de code suivant illustre le comportement de contexte par défaut et l'utilisation de ConfigureAwait :

async Task MyMethodAsync()
{
  // Code here runs in the original context.
  await Task.Delay(1000);
  // Code here runs in the original context.
  await Task.Delay(1000).ConfigureAwait(
    continueOnCapturedContext: false);
  // Code here runs without the original
  // context (in this case, on the thread pool).
}

En utilisant ConfigureAwait, vous autorisez une petite quantité de parallélisme : une partie du code asynchrone peut être exécutée en parallèle avec le thread d'interface utilisateur au lieu de le harceler continuellement avec des petites parties de travail à effectuer.

Outre les performances, ConfigureAwait présente un autre avantage important : il permet d'éviter les blocages. Prenons à nouveau l'exemple de la figure 3. Si vous ajoutez « ConfigureAwait(false) » à la ligne de code de DelayAsync, le blocage est évité. Dans ce cas de figure, une fois await terminé, il tente d'exécuter le reste de la méthode async dans le contexte de pool de threads. La méthode peut se terminer, et la tâche retournée est alors terminée aussi, sans aucun blocage. Cette technique est particulièrement utile si vous avez besoin de convertir progressivement une application synchrone en application asynchrone.

Si vous pouvez utiliser ConfigureAwait à un moment donné dans le cadre d'une méthode, je vous recommande de l'utiliser pour chaque await de cette méthode après ce moment. N'oubliez pas que le contexte est capturé uniquement si une Task non terminée est attendue. Si la Task est déjà terminée, le contexte n'est pas capturé. Certaines tâches peuvent être terminées plus rapidement que prévu selon les situations de matériel et de réseau et vous devez gérer gracieusement une tâche retournée terminée avant d'être attendue. La figure 6 en propose un exemple modifié.

Figure 6 Gestion d'une Task retournée terminée avant d'être attendue

async Task MyMethodAsync()
{
  // Code here runs in the original context.
  await Task.FromResult(1);
  // Code here runs in the original context.
  await Task.FromResult(1).ConfigureAwait(continueOnCapturedContext: false);
  // Code here runs in the original context.
  var random = new Random();
  int delay = random.Next(2); // Delay is either 0 or 1
  await Task.Delay(delay).ConfigureAwait(continueOnCapturedContext: false);
  // Code here might or might not run in the original context.
  // The same is true when you await any Task
  // that might complete very quickly.
}

N'utilisez pas ConfigureAwait lorsqu'il y a du code après await dans la méthode qui a besoin de contexte. Pour les applications GUI, cela inclut tout code qui manipule des éléments d'interface utilisateur, écrit des propriétés liées aux données ou dépend d'un type propre à l'interface utilisateur, par exemple Dispatcher/CoreDispatcher. Pour les applications ASP.NET, cela inclut tout code qui utilise HttpContext.Current ou crée une réponse ASP.NET, dont les instructions de retour dans les actions de contrôleur. La figure 7 montre un schéma commun aux applications GUI : un gestionnaire d'événements asynchrones qui désactive son contrôle au début de la méthode, effectue quelques await, puis réactive son contrôle à la fin du gestionnaire. Le gestionnaire d'événements ne peut pas abandonner son contexte car il doit réactiver son contrôle.

Figure 7 Gestionnaire d'événements asynchrones qui désactive et réactive son contrôle

private async void button1_Click(object sender, EventArgs e)
{
  button1.Enabled = false;
  try
  {
    // Can't use ConfigureAwait here ...
    await Task.Delay(1000);
  }
  finally
  {
    // Because we need the context here.
    button1.Enabled = true;
  }
}

Chaque méthode async a son propre contexte. Par conséquent, si une méthode async appelle une autre méthode async, leurs contextes sont indépendants. La figure 8 reprend le code de la figure 7 et le modifie légèrement.

Figure 8 Chaque méthode async a son propre contexte

private async Task HandleClickAsync()
{
  // Can use ConfigureAwait here.
  await Task.Delay(1000).ConfigureAwait(continueOnCapturedContext: false);
}
private async void button1_Click(object sender, EventArgs e)
{
  button1.Enabled = false;
  try
  {
    // Can't use ConfigureAwait here.
    await HandleClickAsync();
  }
  finally
  {
    // We are back on the original context for this method.
    button1.Enabled = true;
  }
}

Un code sans contexte est plus facilement réutilisable. Essayez de créer dans votre code une barrière entre le code sensible au contexte et celui qui n'en contient pas, puis réduisez la quantité de code sensible au contexte. Dans le cadre du code de la figure 8, je recommande d'insérer la logique de base du gestionnaire d'événements dans une méthode async Task testable et sans contexte, afin d'obtenir uniquement un code minimal dans le gestionnaire d'événements sensible au contexte. Même si vous écrivez une application ASP.NET, si vous disposez d'une bibliothèque de base potentiellement partagée avec d'autres applications de bureau, envisagez l'utilisation de ConfigureAwait dans le code de bibliothèque.

Pour résumer cette troisième recommandation, utilisez ConfigureAwait dès que possible. Le code sans contexte obtient de meilleures performances pour les applications GUI et cette technique est utile pour éviter les blocages lors de l'utilisation d'une base de code partiellement asynchrone. Les méthodes qui requièrent un contexte font exception à cette recommandation.

Connaissez vos outils

Beaucoup d'informations doivent être apprises sur async et await et il est donc naturel de se sentir un peu désorienté. La figure 9 vous permet de consulter rapidement des solutions aux problèmes courants.

Figure 9 Solutions aux problèmes courants d'asynchronie

Problème Solution :
Créer une tâche pour exécuter un nœud Task.Run ou TaskFactory.StartNew (pas le constructeur de Task ni Task.Start)
Créer un wrapper de tâche pour une opération ou un événement TaskFactory.FromAsync ou TaskCompletionSource<T>
Annulation de prise en charge CancellationTokenSource et CancellationToken
Signaler l'avancement IProgress<T> et Progress<T>
Gérer les flux de données Flux de données TPL ou extensions réactives
Synchroniser l'accès à une ressource partagée SemaphoreSlim
Initialiser une ressource de façon asynchrone AsyncLazy<T>
Structures producteur/consommateur compatibles avec async Flux de données TPL ou AsyncCollection<T>

Le premier problème vient de la création de la tâche. Selon toute évidence, une méthode async peut créer une tâche, et il s'agit de l'option la plus facile. Si vous avez besoin d'exécuter du code sur un pool de threads, utilisez Task.Run. Si vous souhaitez créer un wrapper de tâche pour une opération ou un événement asynchrone existant, utilisez TaskCompletionSource<T>. La gestion de l'annulation et l'indication de l'avancement font également partie des problèmes courants. La bibliothèque de classes de base, ou BCL, comprend des types spécifiquement conçus afin de résoudre ces problèmes : CancellationTokenSource/CancellationToken et IProgress<T>/Progress<T>. Le code asynchrone doit utiliser le modèle asynchrone basé sur les tâches, ou TAP (msdn.microsoft.com/library/hh873175), qui explique en détail la création, l'annulation et le signalement de l'avancement des tâches.

La gestion des flux de données asynchrones est un autre problème à résoudre. Certes, les tâches présentent de nombreux avantages, mais elles peuvent uniquement retourner un objet et être exécutées une seule fois. Pour les flux asynchrones, vous pouvez utiliser le flux de données TPL ou les extensions réactives (Rx). Le flux de données TPL crée un maillage qui semble plus superficiel. Quant aux extensions réactives, elles sont plus puissantes et efficaces, mais ont une courbe d'apprentissage plus difficile. Le flux de données TPL et les extensions réactives ont des méthodes compatibles avec async et fonctionnent bien avec le code asynchrone.

Ce n'est pas parce que votre code est asynchrone qu'il est sûr. Les ressources partagées doivent toujours être protégées et, pour compliquer les choses, vous ne pouvez pas attendre depuis l'intérieur d'un verrou. Voici un exemple de code asynchrone susceptible de corrompre l'état partagé s'il est exécuté deux fois, y compris s'il est toujours exécuté sur le même thread :

int value;

Task<int> GetNextValueAsync(int current);

async Task UpdateValueAsync()

{

  value = await GetNextValueAsync(value);

}

Le problème est que la méthode lit la valeur et se suspend au niveau d'await, puis quand elle reprend, elle suppose que la valeur n'a pas changé. Pour résoudre ce problème, la classe SemaphoreSlim a été augmentée avec les surcharges WaitAsync compatibles avec async. La figure 10 illustre l'utilisation de SemaphoreSlim.WaitAsync.

Figure 10 SemaphoreSlim permet une synchronisation asynchrone

SemaphoreSlim mutex = new SemaphoreSlim(1);

int value;

Task<int> GetNextValueAsync(int current);

async Task UpdateValueAsync()

{

  await mutex.WaitAsync().ConfigureAwait(false);

  try

  {

    value = await GetNextValueAsync(value);

  }

  finally

  {

    mutex.Release();

  }

}

Le code asynchrone est souvent utilisé afin d'initialiser une ressource qui est ensuite mise en cache et partagée. Il n'existe aucun type intégré à cet effet, mais Stephen Toub a développé AsyncLazy<T> qui joue un rôle de fusion de Task<T> et Lazy<T>. Le type initial est décrit sur son blog (bit.ly/dEN178) et une version mise à jour est disponible dans ma bibliothèque AsyncEx (nitoasyncex.codeplex.com).

En dernier lieu, certaines structures de données compatibles avec async sont parfois nécessaires. Le flux de données TPL fournit un BufferBlock<T> qui joue le rôle d'une file d'attente producteur/consommateur compatible avec async. D'autre part, AsyncEx fournit AsyncCollection<T>, une version asynchrone de BlockingCollection<T>.

J'espère que les recommandations et les conseils de cet article vous ont été utiles. Async est véritablement une fonctionnalité de langage exceptionnelle et le moment est idéal pour commencer à l'utiliser.

Stephen Cleary est un mari, un père et un programmeur qui habite au nord du Michigan. Il travaille avec le multithread et la programmation asynchrone depuis 16 ans et utilise la prise en charge d'async dans Microsoft .NET Framework depuis la première version CTP. Sa page d'accueil, à partir de laquelle vous pourrez accéder à son blog, se trouve à l'adresse stephencleary.com.

Merci à l'expert technique suivant d'avoir relu cet article : Stephen Toub
Stephen Toub travaille au sein de l'équipe Visual Studio chez Microsoft. Il est spécialisé dans les domaines liés au parallélisme et à l'asynchronie.