J'ai écrit quelques méthodes d'action dans un contrôleur pour tester la différence entre sync et async actions du contrôleur dans le noyau ASP.NET:
[Route("api/syncvasync")]
public class SyncVAsyncController : Controller
{
[HttpGet("sync")]
public IActionResult SyncGet()
{
Task.Delay(200).Wait();
return Ok(new { });
}
[HttpGet("async")]
public async Task<IActionResult> AsyncGet()
{
await Task.Delay(200);
return Ok(new { });
}
}
J'ai ensuite testé en charge le point final de synchronisation:
... suivi du point final asynchrone:
Voici les résultats si j'augmente le délai à 1000 ms
Comme vous pouvez le voir, il n'y a pas beaucoup de différence dans les demandes par seconde - Je m'attendais à ce que le point final asynchrone gère plus de demandes par seconde. Suis-je en train de manquer quelque chose?
Oui, vous manquez le fait que l'async n'est pas une question de vitesse, et n'est que légèrement lié au concept de requêtes par seconde.
Async fait une chose et une seule chose. Si une tâche est attendue, et que cette tâche n'implique pas de travail lié au processeur, et en conséquence, le thread devient inactif, alors, ce thread potentiellement pourrait être libéré pour retourner à la piscine pour effectuer d'autres travaux.
C'est ça. Async en bref. Le but de l'async est d'utiliser les ressources plus efficacement . Dans des situations où vous pourriez avoir des fils attachés, simplement assis là en tapant sur leurs orteils, en attendant que certaines opérations d'E/S se terminent, ils peuvent à la place être chargés d'autres tâches. Il en résulte deux idées très importantes que vous devez internaliser:
Async! = Plus rapide. En fait, async est plus lent . Il y a des frais généraux impliqués dans une opération asynchrone: changement de contexte, les données sont mélangées sur et hors du tas, etc. Cela ajoute du temps de traitement supplémentaire. Même si nous ne parlons que de microsecondes dans certains cas, l'async sera toujours plus lent qu'un processus de synchronisation équivalent. Période. Arrêt complet.
Async ne vous achète rien si votre serveur est en charge. Ce n'est que lorsque votre serveur est stressé que l'async lui donnera une marge de manœuvre bien nécessaire, tandis que la synchronisation pourrait le mettre à genoux. C'est une question d'échelle. Si votre serveur ne répond qu'à une quantité infime de demandes, vous ne verrez probablement jamais de différence par rapport à la synchronisation, et comme je l'ai dit, vous pourriez finir par utiliser plus ironiquement, les ressources en raison des frais généraux impliqués.
Cela ne signifie pas que vous ne devez pas utiliser async. Même si votre application n'est pas populaire aujourd'hui, cela ne signifie pas qu'elle ne sera pas plus tard, et redéfinir tout votre code à ce stade pour prendre en charge l'async sera un cauchemar. Le coût de performance de l'async est généralement négligeable, et si vous en avez besoin, il vous sauvera la vie.
MISE À JOUR
En ce qui concerne le maintien du coût de performance de l'async négligeable, il y a quelques conseils utiles, qui ne sont pas évidents ou qui ne sont pas très bien définis dans la plupart des discussions sur l'async en C #.
Utilisez ConfigureAwait(false)
autant que possible.
await DoSomethingAsync().ConfigureAwait(false);
À peu près chaque appel de méthode asynchrone doit être suivi par ceci, à l'exception de quelques exceptions spécifiques. ConfigureAwait(false)
indique au runtime que vous n'avez pas besoin du contexte de synchronisation préservé pendant l'opération asynchrone. Par défaut, lorsque vous attendez une opération asynchrone, un objet est créé pour conserver les sections locales de thread entre les commutateurs de thread. Cela prend une grande partie du temps de traitement impliqué dans la gestion d'une opération asynchrone et, dans de nombreux cas, est complètement inutile. Le seul endroit où cela compte vraiment est dans des choses comme les méthodes d'action, les threads d'interface utilisateur, etc. - des endroits où il y a des informations liées au thread qui doivent être préservées. Vous ne devez conserver ce contexte qu'une seule fois, aussi longtemps que votre méthode d'action, par exemple, attend une opération asynchrone avec le contexte de synchronisation intact, cette opération elle-même peut effectuer d'autres opérations asynchrones où le contexte de synchronisation n'est pas conservé. Pour cette raison, vous devez limiter les utilisations de await
au minimum dans des choses comme les méthodes d'action, et essayer à la place de regrouper plusieurs opérations asynchrones en une seule méthode asynchrone que cette méthode d'action peut appeler. Cela réduira les frais généraux liés à l'utilisation de l'async. Il convient de noter que cela ne concerne que les actions dans ASP.NET MVC. ASP.NET Core utilise un modèle d'injection de dépendances au lieu de statiques, il n'y a donc pas de sections locales de thread à craindre. Dans d'autres, vous pouvez utiliser ConfigureAwait(false)
dans une action ASP.NET Core, mais pas dans ASP.NET MVC. En fait, si vous essayez, vous obtiendrez une erreur d'exécution.
Autant que possible, vous devez réduire la quantité de locaux qui doivent être préservés. Les variables que vous initialisez avant d'appeler wait sont ajoutées au segment de mémoire et sautées une fois la tâche terminée. Plus vous en avez déclaré, plus cela va sur le tas. En particulier, les grands graphiques d'objets peuvent faire des ravages ici, car c'est une tonne d'informations à déplacer sur et hors du tas. Parfois, c'est inévitable, mais c'est quelque chose à garder à l'esprit.
Lorsque cela est possible, élidez les mots clés async
/await
. Prenons l'exemple suivant:
public async Task DoSomethingAsync()
{
await DoSomethingElseAsync();
}
Ici, DoSomethingElseAsync
renvoie un Task
attendu et non encapsulé. Ensuite, un nouveau Task
est créé pour revenir de DoSometingAsync
. Cependant, si à la place, vous avez écrit la méthode comme suit:
public Task DoSomethingAsync()
{
return DoSomethingElseAsync();
}
Le Task
retourné par DoSomethingElseAsync
est retourné directement par DoSomethingAsync
. Cela réduit une quantité importante de frais généraux.
N'oubliez pas que async
concerne davantage la mise à l'échelle que les performances. Vous n'allez pas voir des améliorations dans la capacité de votre application à évoluer en fonction de votre test de performance que vous avez ci-dessus. Pour tester correctement la mise à l'échelle, vous devez effectuer un test de charge dans un environnement approprié qui idéalement correspond à votre environnement de production.
Vous essayez de comparer les améliorations des performances basées sur l'async seul. Il est certainement possible (selon le code/l'application) que vous constatiez une diminution apparente des performances. Cela est dû au fait qu'il y a une surcharge dans le code asynchrone (changement de contexte, machines d'état, etc.). Cela étant dit, 99% du temps, vous devez écrire votre code à l'échelle (encore une fois, en fonction de votre application) - plutôt que de vous soucier des millisecondes supplémentaires passées ici ou là. Dans ce cas, vous ne voyez pas la forêt pour les arbres pour ainsi dire. Vous devriez vraiment vous préoccuper du test de charge plutôt que du microsurveillance lorsque vous testez ce que async
peut faire pour vous.