Partager via



Juillet 2015

Volume 30, numéro 7

Programmation asynchrone - développement de sites urbains contaminés réhabilitables Async

Par Stephen Cleary | Juillet 2015

Quand le Visual Studio Async CTP est sorti, j'ai été dans une situation de chance. J'ai été le seul développeur pour deux applications de greenfield relativement petit qui profiterait d'async et attendent. Pendant ce temps, divers membres des forums MSDN dont je fais partie ont été découvrir, discuter et mise en œuvre de plusieurs pratiques exemplaires asynchrones. Le plus important de ces pratiques est compilé dans mon mars 2013 Magazinearticle MSDN, « Les meilleures pratiques en programmation asynchrone » (msdn.microsoft.com/magazine/jj991977).

Appliquant async et attendent d'un code existant base est un autre type de défi. Code de friches industrielles peut être salissant, qui complique encore le scénario. Quelques techniques que j'ai trouvé utile lors de l'application async brownfield code que je vais vous expliquer ici. Introduction async peut réellement affecter la conception dans certains cas. S'il y a tout refactorisation nécessaire pour séparer le code existant en couches, je recommande de faire qu'avant d'introduire async. Aux fins du présent article, je vais supposer que vous utilisez une architecture d'application similaire à ce qui est montré dans Figure 1.

Figure 1 Structure de Code Simple avec une couche de Service et de la couche de logique métier

public interface IDataService
{
  Task<string> GetAsync(int id);
}
public sealed class WebDataService : IDataService
{
  public async Task<string> GetAsync(int id)
  {
    using (var client = new WebClient())
      return await client.DownloadStringTaskAsync(
      "http://www.example.com/api/values/" + id);
  }
}
public sealed class BusinessLogic
{
  private readonly IDataService _dataService;
  public BusinessLogic(IDataService dataService)
  {
    _dataService = dataService;
  }
  public async Task<string> GetFrobAsync()
  {
    // Try to get the new frob id.
    var result = await _dataService.GetAsync(17);
    if (result != string.Empty)
      return result;
    // If the new one isn't defined, get the old one.
    return await _dataService.GetAsync(13);
  }
}

Quand utiliser Async

La meilleure approche générale est à la première réflexion sur ce que fait réellement l'application. Async excelle dans les opérations d'e/S, mais il y a parfois des meilleures options pour les autres types de traitement. Il existe deux scénarios peu communes où async n'est pas un ajustement parfait — flux de code et les données liées aux UC.

Si vous avez lié aux CPU code, examiner la classe parallèle ou Parallel LINQ. Async est plus adapté à un système basé sur des événements, où il n'y a aucun code réel qui s'exécute lorsqu'une opération est en cours. Liées aux UC code dans une méthode async s'exécutera de façon synchrone.

Toutefois, vous pouvez traiter liées aux UC code comme si c'était asynchrone en attente de l'issue des Task.Run. Il s'agit d'une bonne façon de pousser les CPU lié aux travaux sur le thread d'interface utilisateur. Le code suivant est un exemple d'utilisation de Task.Run comme un pont entre code asynchrone et parallèle :

await Task.Run(() => Parallel.ForEach(...));

L'autre scénario où async n'est pas le mieux adapté est lorsque votre application traite de flux de données. Opérations asynchrones ont un certain commencement et la fin. Par exemple, un téléchargement de la ressource démarre lorsque la ressource est demandée. Il se termine lorsque le téléchargement de la ressource est terminée. Si vos données entrantes soient plus d'un flux ou un abonnement, async ne peut-être pas la meilleure approche. Considérons un périphérique connecté à un port série qui peut volontaire de données à tout moment, à titre d'exemple.

Il est possible d'utiliser async/attendent avec flux d'événements. Il faudra certaines ressources du système pour la mise en mémoire tampon des données qu'il arrive jusqu'à ce que l'application lit les données. Si votre source est un abonnement aux événements, pensez à utiliser les Reactive Extensions ou TPL Dataflow. Vous trouverez peut-être un choix plus naturel que la plaine async. Les Rx et les flux de données interagissent bien avec code asynchrone.

Async est certainement la meilleure approche pour beaucoup de code, juste ne pas tout ça. Pour le reste de cet article, je supposerai que vous avez envisagé la Task Parallel Library et Rx/Dataflow et avez conclu qu'async/attendent est l'approche la plus appropriée.

Transformation synchrone au Code asynchrone

Il y a une procédure normale pour la conversion de code synchrone existant en code asynchrone. Il est relativement simple. Il peut même devenir assez fastidieux une fois que vous avez fait quelques fois. A ce jour, il n'y a aucun support pour automatique synchrone-à-­conversion asynchrone. Cependant, je m'attends à que ce genre de transformation de code est introduit dans les années à venir.

Cette procédure fonctionne mieux lorsque vous commencez aux couches de niveau inférieur et travaillez votre chemin vers les niveaux de l'utilisateur. En d'autres termes, commencez à introduire async dans les méthodes de couche de données qui accèdent à une base de données ou les API Web. Ensuite introduire async dans vos méthodes de service, puis la logique métier et, enfin, la couche de l'utilisateur. Si votre code ne possède des couches bien définies, vous pouvez toujours convertir async/attendre. Ce sera juste un peu plus difficile.

La première étape consiste à identifier l'opération asynchrone naturellement à basse altitude pour convertir. Tout ce que j'ai/O-basé est un candidat de choix pour une opération asynchrone. Des exemples courants sont les requêtes de base de données et des commandes, des appels d'API Web et accès aux fichiers système. Plusieurs fois, cette opération à basse altitude déjà a une API asynchrone existante.

Si la bibliothèque sous-jacente a une API asynchrone-prêt, tout ce que vous devez faire est ajouter un suffixe Async (ou TaskAsync) sur le nom de la méthode synchrone. Par exemple, un appel de Entity Framework à premièrement peut être remplacé par un appel à FirstAsync. Dans certains cas, vous pouvez utiliser un autre type. Par exemple, HttpClient est un remplacement plus facile à async WebClient et HttpWebRequest. Dans certains cas, vous devrez peut-être mettre à niveau la version de votre bibliothèque. Entity Framework, par exemple, acquis un async API en version 6.

Examinons le code dans Figure 1. Il s'agit d'un exemple simple d'une couche de service et une logique métier. Dans cet exemple, il n'y a qu'une seule opération à basse altitude — récupérer une chaîne d'identificateur de FRAB d'une API Web à WebDataService.Get. Il s'agit de l'endroit logique pour commencer la conversion asynchrone. Dans ce cas, le développeur peut choisir de remplacer WebClient.DownloadString avec WebClient.DownloadStringTaskAsync, ou de remplacer le WebClient avec le plus respectueux de l'async HttpClient.

La deuxième étape consiste à modifier l'appel d'API synchrone pour un appel d'API asynchrone et ensuite attendre la tâche retournée. Lorsque le code appelle une méthode asynchrone, il convient généralement d'attendre la tâche retournée. À ce stade, le compilateur va se plaindre. Le code suivant provoquera une erreur du compilateur avec le message, "l'opérateur « attendre » utilisable uniquement dans une méthode async. Envisager de marquer cette méthode avec le modificateur « async » et de changer son type de retour 'Tâche < string >' ":

public sealed class WebDataService : IDataService
{
  public string Get(int id)
  {
    using (var client = new WebClient())
      return await client.DownloadStringTaskAsync(
      "http://www.example.com/api/values/" + id);
  }
}

Le compilateur vous guide vers l'étape suivante. Mark la méthode async et le changement du type de retour. Si le type de retour de la méthode synchrone est nul, alors le type de retour de la méthode asynchrone doit être opérationnelle. Sinon, pour tout type de retour de méthode synchrone de T, le type de retour de méthode asynchrone doit être tâche < T >. Lorsque vous modifiez le type de retour pour la tâche/tâche < T >, vous devez également modifier le nom de la méthode se terminer en Async, de suivre les directives de Task-Based Asynchronous Pattern. Le code suivant illustre la méthode résultante comme une méthode asynchrone :

public sealed class WebDataService : IDataService
{
  public async Task<string> GetAsync(int id)
  {
    using (var client = new WebClient())
      return await client.DownloadStringTaskAsync(
      "http://www.example.com/api/values/" + id);
  }
}

Avant de poursuivre, consultez le reste de cette méthode pour n'importe quel autres blocage ou synchrones appels API que vous pouvez faire async. Méthodes asynchrones ne doit pas bloquer, donc cette méthode doit appeler l'API asynchrones si elles sont disponibles. Dans cet exemple simple, il n'y a aucun autre appel bloquant. Dans le monde réel code, gardez un œil sur pour la logique de nouvelle tentative et de résolution des conflits optimiste.

Entity Framework devrait obtenir une mention spéciale ici. Une subtile « chasse aux sorcières » est différée chargement des entités associées. Cela se fait toujours de façon synchrone. Si possible, utilisez des requêtes asynchrones explicites supplémentaires au lieu de chargement différé.

Maintenant, cette méthode est enfin terminée. Ensuite, passer à toutes les méthodes qui font référence à celui-ci et suivez la procédure à nouveau. Dans ce cas, WebDataService.Get faisait partie d'une implémentation de l'interface, donc vous devez modifier l'interface pour permettre des implémentations asynchrones :

public interface IDataService
{
  Task<string> GetAsync(int id);
}

Ensuite, passer à des méthodes d'appel et suivez les mêmes étapes. Vous devriez retrouver avec quelque chose comme le code de Figure 2. Malheureusement, le code ne sera pas compilé jusqu'à ce que toutes les méthodes d'appel sont transformés en async, et puis tous leurs méthodes d'appel sont transformés en async et ainsi de suite. Ce caractère en cascade d'async est l'aspect lourd de développement de friches industrielles.

Figure 2 changements toutes les méthodes d'appel Async

public interface IDataService
{
  string Get(int id);
}
public sealed class WebDataService : IDataService
{
  public string Get(int id)
  {
    using (var client = new WebClient())
      return client.DownloadString("http://www.example.com/api/values/" + id);
  }
}
public sealed class BusinessLogic
{
  private readonly IDataService _dataService;
  public BusinessLogic(IDataService dataService)
  {
    _dataService = dataService;
  }
  public string GetFrob()
  {
    // Try to get the new frob id.
    var result = _dataService.Get(17);
    if (result != string.Empty)
      return result;
    // If the new one isn't defined, get the old one.
    return _dataService.Get(13);
  }
}

Finalement, le niveau d'opération asynchrone dans votre base de code passera jusqu'à ce qu'il frappe une méthode qui n'est pas appelée par une autre méthode dans votre code. Vos méthodes de niveau supérieur sont appelés directement par n'importe quel framework que vous utilisez. Certains cadres tels que ASP.NET MVC permettent de code asynchrone directement. Par exemple, les actions du contrôleur MVC ASP.NET peuvent retourner Task ou Task < T >. Autres cadres tels que Windows Presentation Foundation (WPF) permettent des gestionnaires d'événements asynchrones. Ainsi, par exemple, un événement de clic button pourrait être async Sub.

Frapper le mur

Comme le niveau de code asynchrone se développe tout au long de votre application, vous pourriez atteindre un point où il semble impossible de continuer. Les exemples les plus courants de ce sont des constructions orientée objet, qui ne cadrent avec la nature fonctionnelle du code asynchrone. Propriétés, les événements et les constructeurs ont leurs propres défis.

Repenser la conception est généralement le meilleur moyen autour de ces difficultés. Constructeurs en est un exemple commun. Dans le code synchrone, une méthode de constructeur peut se bloquer sur I/O. Dans le monde asynchrone, une solution consiste à utiliser une méthode de fabrique asynchrone au lieu d'un constructeur. Autre exemple : propriétés. Si une propriété synchrone bloque sur i/o, que la propriété devrait probablement être une méthode. Un exercice de conversion asynchrone est grand à dénoncer ces genres de problèmes de conception qui s'insinuer dans votre base de code au fil du temps.

Conseils de transformation

Effectuer une transformation de code asynchrone peut être effrayant les premières fois, mais elle devient vraiment une seconde nature après un peu de pratique. Comme vous vous sentez plus à l'aise avec la conversion de code code synchrone asynchrone, voici quelques conseils, que vous pouvez commencer à utiliser pendant le processus de conversion.

Que vous convertissez votre code, gardez un œil sur des occasions d'accès concurrentiel. Code asynchrone simultané est souvent plus court et plus simple que le code simultané synchrone. Par exemple, considérez une méthode ayant pour télécharger deux ressources différentes d'une API REST. La version synchrone de cette méthode serait presque certainement télécharger un, puis l'autre. Toutefois, la version asynchrone pourrait facilement commencer les téléchargements et puis attente asynchrone pour les deux à compléter à l'aide de Task.WhenAll.

Une autre considération est une annulation. Habituellement, les utilisateurs de l'application synchrone servent à attente. Si l'interface utilisateur est conforme à la nouvelle version, ils pourraient s'attendre à la possibilité d'annuler l'opération. Code asynchrone doit généralement en charge l'annulation sauf s'il existe une autre raison, qu'il ne peut pas. Pour l'essentiel, votre propre code asynchrone peut prendre en charge annulation juste en prenant un argument CancellationToken et en passant à travers les méthodes asynchrones, qu'il appelle.

Vous pouvez convertir n'importe quel code à l'aide de Thread ou un BackgroundWorker à utiliser Task.Run au lieu de cela. Task.Run est beaucoup plus facile à composer que le Thread ou un BackgroundWorker. Par exemple, il est beaucoup plus facile à exprimer, « commencent deux calculs de fond et puis ce faire autre chose quand ils ont tous deux réussi, » avec l'attente moderne et Task.Run, qu'avec primitif enfilage des constructions.

Partitions verticales

L'approche décrite jusqu'ici fonctionne très bien si vous êtes le seul développeur pour votre application, et vous n'avez pas de questions ou demandes qui interféreraient avec votre travail de conversion asynchrone. Ce n'est pas très réaliste, cependant, est-ce ?

Si vous n'avez pas le temps de convertir votre base de code complet pour être asynchrone, tout à la fois, vous pouvez approcher la conversion avec une légère modification appelée partitions verticales. En utilisant cette technique, vous pouvez faire votre conversion asynchrone à certaines sections du code. Partitions verticales sont idéales si vous souhaitez juste « essayer » les code asynchrone.

Pour créer une partition verticale, identifiez le code de niveau utilisateur que vous souhaitez rendre asynchrone. C'est peut-être le gestionnaire d'événements d'un bouton de l'interface utilisateur qui enregistre dans une base de données (où vous ne souhaitez pas que l'interface utilisateur réactive), ou une demande ASP.NET fortement utilisée qui fait la même chose (où vous souhaitez réduire les ressources nécessaires à cette demande spécifique). Parcourir le code, portant sur l'arborescence des appels pour cette méthode. Ensuite, vous pouvez commencer aux méthodes de bas niveau et transformer votre chemin jusqu'à l'arbre.

Autre code utilisera sans doute les mêmes méthodes de bas niveau. Parce que vous n'êtes pas prêt à faire tout ce code asynchrone, la solution consiste à créer une copie de la méthode. Puis transformer cette copie pour être asynchrone. De cette façon, la solution peut toujours construire à chaque étape. Lorsque vous avez travaillé votre chemin jusqu'à le code au niveau utilisateur, vous vous avez créé une partition verticale de code asynchrone dans votre application. Une partition verticale fondée sur notre exemple de code apparaîtrait comme le montre Figure 3.

Figure 3 utilisation de Partitions verticales à convertir des Sections de Code Async

public interface IDataService
{
  string Get(int id);
  Task<string> GetAsync(int id);
}
public sealed class WebDataService : IDataService
{
  public string Get(int id)
  {
    using (var client = new WebClient())
      return client.DownloadString("http://www.example.com/api/values/" + id);
  }
  public async Task<string> GetAsync(int id)
  {
    using (var client = new WebClient())
      return await client.DownloadStringTaskAsync(
      "http://www.example.com/api/values/" + id);
  }
}
public sealed class BusinessLogic
{
  private readonly IDataService _dataService;
  public BusinessLogic(IDataService dataService)
  {
    _dataService = dataService;
  }
  public string GetFrob()
  {
    // Try to get the new frob id.
    var result = _dataService.Get(17);
    if (result != string.Empty)
      return result;
    // If the new one isn't defined, get the old one.
    return _dataService.Get(13);
  }
  public async Task<string> GetFrobAsync()
  {
    // Try to get the new frob id.
    var result = await _dataService.GetAsync(17);
    if (result != string.Empty)
      return result;
    // If the new one isn't defined, get the old one.
    return await _dataService.GetAsync(13);
  }
}

Vous avez peut-être remarqué qu'il y a des doublons de code avec cette solution. Toute la logique pour les méthodes synchrones et asynchrones est dupliquée, qui n'est pas bon. Dans un monde parfait, la duplication de code pour cette partition verticale n'est que temporaire. Le code dupliqué existerait seulement dans votre contrôle de code source jusqu'à ce que l'application a été complètement transformée. À ce stade, vous pouvez supprimer toutes les anciennes API synchrones.

Toutefois, vous ne pouvez faire cela dans toutes les situations. Si vous développez une bibliothèque (même un utilisé uniquement en interne), la compatibilité descendante est une préoccupation majeure. Vous pourriez trouver vous-même qui ont besoin de maintenir des APIs synchrones depuis un certain temps.

Il y a trois réponses possibles à cette situation. Tout d'abord, vous pourriez conduire adoption d'API asynchrones. Si votre bibliothèque possède asynchrone de travail à faire, il doit exposer API asynchrones. Deuxièmement, vous pourriez accepter la duplication de code comme un mal nécessaire pour la compatibilité descendante. Il s'agit d'une solution acceptable uniquement si votre équipe a autodiscipline exceptionnelle ou si la contrainte de compatibilité descendante n'est que temporaire.

La troisième solution consiste à appliquer un des hacks décrites ici. Alors que je ne peux pas vraiment recommander aucun de ces hacks, ils peuvent être utiles à la rigueur. Parce que leur fonctionnement est naturellement asynchrone, chacune de ces hacks est orienté autour de fournissant une API synchrone pour une opération asynchrone naturellement, qui est un anti-modèle bien connu décrit plus en détail dans un serveur & Outils Articles post à bit.ly/1JDLmWD.

Le Hack de blocage

La plus simple consiste à bloquer tout simplement la version asynchrone. Je vous recommande de bloquer avec GetAwaiter(). GetResult au lieu d'attendre ou de résultat. Attendre et résultat enveloppera toutes les exceptions dans une AggregateException, ce qui complique la gestion des erreurs. Le code de couche de service exemple ressemblerait le code indiqué dans Figure 4 si elle utilisait le hack de blocage.

Figure 4 Code de couche de Service utilisant l'entaille bloquant

public sealed class WebDataService : IDataService
{
  public string Get(int id)
  {
    return GetAsync(id).GetAwaiter().GetResult();
  }
  public async Task<string> GetAsync(int id)
  {
    // This code will not work as expected.
    using (var client = new WebClient())
      return await client.DownloadStringTaskAsync(
      "http://www.example.com/api/values/" + id);
  }
}

Malheureusement, comme l'indique le commentaire, ce code ne fonctionne réellement. Il en résulte un blocage commun décrit dans mon article « Les meilleures pratiques en programmation asynchrone » je l'ai mentionné plus tôt.

C'est où le hack peut se complique. Un test unitaire normal va passer, mais le même code se bloqueront si appelé par un contexte UI ou ASP.NET . Si vous utilisez le hack de blocage, vous devez écrire des tests unitaires qui vérifier ce comportement. Le code dans Figure 5 utilise l'Async­type de contexte de ma bibliothèque de AsyncEx, qui crée un contexte similaire à un contexte UI ou ASP.NET .

Figure 5 utilisation du Type AsyncContext

[TestClass]
public class WebDataServiceUnitTests
{
  [TestMethod]
  public async Task GetAsync_RetrievesObject13()
  {
    var service = new WebDataService();
    var result = await service.GetAsync(13);
    Assert.AreEqual("frob", result);
  }
  [TestMethod]
  public void Get_RetrievesObject13()
  {
    AsyncContext.Run(() =>
    {
      var service = new WebDataService();
      var result = service.Get(13);
      Assert.AreEqual("frob", result);
    });
  }
}

Malheureusement, comme l'indique le commentaire, ce code ne fonctionne réellement. Il en résulte un blocage commun décrit dans mon article « Les meilleures pratiques en programmation asynchrone » je l'ai mentionné plus tôt.

C'est où le hack peut se complique. Un test unitaire normal va passer, mais le même code se bloqueront si appelé par un contexte UI ou ASP.NET . Si vous utilisez le hack de blocage, vous devez écrire des tests unitaires qui vérifier ce comportement. Le code dans Figure 5 utilise l'Async­type de contexte de ma bibliothèque de AsyncEx, qui crée un contexte similaire à un contexte UI ou ASP.NET .

Figure 5 utilisation du Type AsyncContext

[TestClass]
public class WebDataServiceUnitTests
{
  [TestMethod]
  public async Task GetAsync_RetrievesObject13()
  {
    var service = new WebDataService();
    var result = await service.GetAsync(13);
    Assert.AreEqual("frob", result);
  }
  [TestMethod]
  public void Get_RetrievesObject13()
  {
    AsyncContext.Run(() =>
    {
      var service = new WebDataService();
      var result = service.Get(13);
      Assert.AreEqual("frob", result);
    });
  }
}

L'unité asynchrone test passes, mais le test unitaire synchrones ne se termine jamais. C'est le problème de blocage classiques. Le code asynchrone capture le contexte actuel et tente de reprendre à ce sujet, tandis que le wrapper synchrone bloque un thread dans ce contexte, prévenir l'opération asynchrone de se terminer.

Dans ce cas, notre code asynchrone manque un ConfigureAwait­(faux). Cependant, le même problème peut résulter de l'utilisation de WebClient. WebClient utilise le plus vieux événement-modèle asynchrone basé sur (EAP), qui capte toujours le contexte. Donc même si votre code utilise ConfigureAwait(false), la même impasse se produira à partir du code de WebClient. Dans ce cas, vous pouvez remplacer WebClient avec le plus facile à async HttpClient et que cela fonctionne sur le bureau, comme le montre Figure 6.

Figure 6 utilisation HttpClient avec ConfigureAwait(false) pour empêcher l'interblocage

public sealed class WebDataService : IDataService
{
  public string Get(int id)
  {
    return GetAsync(id).GetAwaiter().GetResult();
  }
  public async Task<string> GetAsync(int id)
  {
    using (var client = new HttpClient())
      return await client.GetStringAsync(
      "http://www.example.com/api/values/" + id).ConfigureAwait(false);
  }
}

Le hack de blocage nécessite votre équipe d'avoir une discipline stricte. Ils doivent s'assurer que ConfigureAwait(false) est utilisé dans le monde. Ils doivent aussi exiger toutes les bibliothèques dépendantes à suivre la même discipline. Dans certains cas, cela n'est pas possible. A ce jour, même HttpClient capture le contexte sur certaines plateformes.

Un autre inconvénient pour le hack de blocage est il vous oblige à utiliser ConfigureAwait­(faux). Il convient tout simplement pas si le code asynchrone a réellement besoin reprendre le contexte capturé. Si vous adoptez le hack de blocage, vous êtes fortement recommandé d'exécuter des tests unitaires à l'aide de AsyncContext ou un autre contexte de thread similaire pour attraper les blocages latents.

Le Hack de Pool de Thread

Une approche similaire pour le Hack de blocage consiste à décharger le travail asynchrone dans le pool de threads, puis bloc sur la tâche qui en résulte. Le code à l'aide de ce hack ressemblerait le code indiqué dans Figure 7.

Figure 7 Code pour le Hack de Pool de Thread

public sealed class WebDataService : IDataService
{
  public string Get(int id)
  {
    return Task.Run(() => GetAsync(id)).GetAwaiter().GetResult();
  }
  public async Task<string> GetAsync(int id)
  {
    using (var client = new WebClient())
      return await client.DownloadStringTaskAsync(
      "http://www.example.com/api/values/" + id);
  }
}

L'appel à Task.Run exécute la méthode asynchrone sur un thread ThreadPool. Ici, il s'exécutera sans un contexte, afin d'éviter l'impasse. Un des problèmes avec cette approche est asynchrone méthode ne saurait dépendre de l'exécution dans un contexte spécifique. Ainsi, elle ne peut utiliser des éléments d'interface utilisateur ou la HttpContext.Current ASP.NET .

Un autre, plus subtile « gotcha » est que la méthode asynchrone peut reprendre sur n'importe quel thread ThreadPool. Ce n'est pas un problème pour la plupart du code. Il peut être un problème si la méthode utilise l'état du thread par thread ou s'il dépend implicitement la synchronisation fournie par un contexte d'interface utilisateur.

Vous pouvez créer un contexte d'un thread d'arrière-plan. Le type de AsyncContext dans ma bibliothèque AsyncEx installera un contexte monothread avec une « boucle principale ». Cela oblige le code asynchrone de reprendre sur le même thread. Cela permet d'éviter les « pièges » plus subtile de l'entaille de pool de thread. L'exemple de code avec une boucle principale pour le thread ThreadPool ressemblerait le code indiqué dans Figure 8.

Figure 8 utilisation une boucle principale pour le Hack de Pool de Thread

public sealed class WebDataService : IDataService
{
  public string Get(int id)
  {
    var task = Task.Run(() => AsyncContext.Run(() => GetAsync(id)));
    return task.GetAwaiter().GetResult();
  }
  public async Task<string> GetAsync(int id)
  {
    using (var client = new WebClient())
      return await client.DownloadStringTaskAsync(
      "http://www.example.com/api/values/" + id);
  }
}

Bien sûr, il y a un inconvénient de cette approche, aussi bien. Le thread du pool est bloqué au sein de la AsyncContext jusqu'à la fin de la méthode asynchrone. Ce thread bloqué est là, ainsi que le thread principal en appelant l'API synchrone. Ainsi, pendant la durée de l'appel, il y a deux threads bloqués. Sur ASP.NET en particulier, cette approche réduira considérablement capacité de l'application à l'échelle.

Le drapeau Argument Hack

Ce hack est un que je n'ai pas encore utilisé. Il a été décrit pour moi par Stephen Toub lors de son examen de tech du présent article. C'est une excellente approche et mon préféré de tous ces hacks.

Le drapeau argument hack prend la méthode originale, rend privé et ajoute un indicateur pour indiquer si la méthode doit s'exécuter de manière synchrone ou asynchrone. Il expose ensuite deux API publiques, synchrone de l'un et l'autre asynchrones, comme le montre Figure 9.

Figure 9 drapeau Argument Hack expose les deux API

public interface IDataService
{
  string Get(int id);
  Task<string> GetAsync(int id);
}
public sealed class WebDataService : IDataService
{
  private async Task<string> GetCoreAsync(int id, bool sync)
  {
    using (var client = new WebClient())
    {
      return sync
        ? client.DownloadString("http://www.example.com/api/values/" + id)
        : await client.DownloadStringTaskAsync(
        "http://www.example.com/api/values/" + id);
    }
  }
  public string Get(int id)
  {
    return GetCoreAsync(id, sync: true).GetAwaiter().GetResult();
  }
  public Task<string> GetAsync(int id)
  {
    return GetCoreAsync(id, sync: false);
  }
}

La méthode GetCoreAsync dans cet exemple a une propriété importante — si son argument sync est true, elle renvoie toujours un déjà-­tâche achevée. La méthode se bloque lorsque son argument drapeau demande comportement synchrone. Dans le cas contraire, il agit comme une méthode asynchrone normale.

Le wrapper de Get synchrone passe de true à l'argument du drapeau et puis récupère le résultat de l'opération. Remarque Il n'y a aucune chance d'un blocage car la tâche est déjà terminée. La logique métier suit une tendance similaire, comme indiqué dans Figure 10.

Figure 10 appliquer drapeau Argument entaille à la logique métier

public sealed class BusinessLogic
{
  private readonly IDataService _dataService;
  public BusinessLogic(IDataService dataService)
  {
    _dataService = dataService;
  }
  private async Task<string> GetFrobCoreAsync(bool sync)
  {
    // Try to get the new frob id.
    var result = sync
      ? _dataService.Get(17)
      : await _dataService.GetAsync(17);
    if (result != string.Empty)
      return result;
    // If the new one isn't defined, get the old one.
    return sync
      ? _dataService.Get(13)
      : await _dataService.GetAsync(13);
  }
  public string GetFrob()
  {
    return GetFrobCoreAsync(sync: true).GetAwaiter().GetResult();
  }
  public Task<string> GetFrobAsync()
  {
    return GetFrobCoreAsync(sync: false);
  }
}

Vous n'avez pas la possibilité d'exposer les méthodes CoreAsync de votre couche de service. Cela simplifie la logique métier. Toutefois, la méthode d'argument drapeau est plus d'un détail d'implémentation. Vous devez peser l'avantage d'un code plus propre contre l'inconvénient d'exposer les détails d'implémentation, comme le montre Figure 11. L'avantage de ce hack est que la logique des méthodes reste fondamentalement la même chose. Il appelle simplement différentes API basé sur la valeur de l'argument de l'indicateur. Cela fonctionne très bien si il y a une correspondance biunivoque entre les API synchrones et asynchrones, ce qui est généralement le cas.

Figure 11 détails d'implémentation sont exposés, mais le Code est propre

public interface IDataService
{
  string Get(int id);
  Task<string> GetAsync(int id);
  Task<string> GetCoreAsync(int id, bool sync);
}
public sealed class BusinessLogic
{
  private readonly IDataService _dataService;
  public BusinessLogic(IDataService dataService)
  {
    _dataService = dataService;
  }
  private async Task<string> GetFrobCoreAsync(bool sync)
  {
    // Try to get the new frob id.
    var result = await _dataService.GetCoreAsync(17, sync);
    if (result != string.Empty)
      return result;
    // If the new one isn't defined, get the old one.
    return await _dataService.GetCoreAsync(13, sync);
  }
  public string GetFrob()
  {
    return GetFrobCoreAsync(sync: true).GetAwaiter().GetResult();
  }
  public Task<string> GetFrobAsync()
  {
    return GetFrobCoreAsync(sync: false);
  }
}

Il peut ne pas fonctionner aussi bien si vous souhaitez ajouter simultanéité à votre chemin de code asynchrone, ou s'il n'y a pas une API asynchrone correspondante idéale. Par exemple, je préfère utiliser HttpClient sur WebClient dans WebDataService, mais je dois qui pèsent contre la complexité supplémentaire que cela provoquerait dans la méthode GetCoreAsync.

L'inconvénient principal de ce hack est drapeau arguments sont un anti-modèle bien connu. Indicateur booléen arguments sont qu'un bon indicateur, une méthode est vraiment deux méthodes différentes d'un. Toutefois, l'anti-modèle est minimisé dans les détails d'implémentation d'une classe unique (sauf si vous choisissez d'exposer vos méthodes de CoreAsync). Malgré cela, c'est toujours mon préféré des entailles.

Le Hack de boucle de Message imbriquée

Cette entaille finale est mon préféré moins. L'idée est que vous mis en place une boucle de message imbriquée dans le thread d'interface utilisateur et exécutez le code asynchrone dans cette boucle. Cette approche n'est pas une option sur ASP.NET. Il peut également exiger le code différent pour différentes plates-formes d'interface utilisateur. Par exemple, une application WPF peut utiliser frames imbriqués répartiteur, alors qu'une application Windows Forms peut utiliser DoEvents dans une boucle. Si des méthodes asynchrones ne dépendent d'une plate-forme d'interface utilisateur particulière, vous pouvez également utiliser AsyncContext pour exécuter une boucle imbriquée, comme le montre Figure 12.

Figure 12 exécuter un Message imbriquée avec AsyncContext

public sealed class WebDataService : IDataService
{
  public string Get(int id)
  {
    return AsyncContext.Run(() => GetAsync(id));
  }
  public async Task<string> GetAsync(int id)
  {
    using (var client = new WebClient())
      return await client.DownloadStringTaskAsync(
      "http://www.example.com/api/values/" + id);
  }
}

Don' t être trompé par la simplicité de cet exemple de code. Ce hack est le plus dangereux de tous, parce que vous devez considérer la réentrance. Cela est particulièrement vrai si le code utilise les frames imbriqués répartiteur ou DoEvents. Dans ce cas, la couche d'interface utilisateur entière doit maintenant gérer réentrance inattendue. Reentrant sécurisé des applications exigent une somme considérable d'une réflexion approfondie et de planification.

Jaquette en haut

Dans un monde idéal, vous pourriez effectuer une transformation de code relativement simple de synchrone à asynchrone et tout serait arcs-en-ciel et licornes. Dans le monde réel, il est souvent nécessaire pour le code synchrone et asynchrone de coexister. Si vous voulez juste essayer async, créez une partition verticale (avec duplication de code) jusqu'à ce que vous êtes à l'aise en utilisant async. Si vous devez conserver le code synchrone pour des raisons de compatibilité descendante, vous devrez vivre avec la duplication de code ou d'appliquer un des hacks.

Un jour, les opérations asynchrones seront seulement représentées avec API asynchrones. D'ici là, vous avez à vivre dans le monde réel. J'espère que ces techniques vous aideront à adopter l'async dans vos applications existantes d'une manière qui vous convient le mieux.


Stephen Cleary est un mari, le père et le programmeur vivant dans le nord du Michigan. Il a travaillé avec multithreading et asynchrone de programmation pendant 16 ans et a utilisé soutien asynchrones dans le Microsoft .NET Framework depuis la première version CTP. Suivre ses projets et de messages de blog à stephencleary.com.

Remercie les experts techniques Microsoft suivants pour avoir relu cet article : James McCaffery et Stephen Toub