Tout le monde sait que l’asynchronie offre un “meilleur débit”, une “scalabilité” et une consommation de ressources plus efficace. Je pensais aussi de cette manière (simpliste) avant de réaliser l’expérience ci-dessous. Elle montre essentiellement que si l’on prend en compte toute la surcharge du code asynchrone et qu’on le compare à du code synchrone correctement configuré, cela n’apporte que peu ou pas d’avantages en termes de performances/débit/consommation de ressources.
La question : le code asynchrone est-il vraiment tellement plus performant comparé au code synchrone avec un pool de threads correctement configuré ? Peut-être que mes tests de performance sont fondamentalement biaisés ?
Configuration du test : deux méthodes ASP.NET Web API avec JMeter essayant de les appeler avec un groupe de 200 threads (temps de montée en charge de 30 secondes).
[HttpGet]
[Route("async")]
public async Task<string> AsyncTest()
{
await Task.Delay(_delayMs);
return "ok";
}
[HttpGet]
[Route("sync")]
public string SyncTest()
{
Thread.Sleep(_delayMs);
return "ok";
}
Voici le temps de réponse (échelle logarithmique). Remarquez comment le code synchrone devient plus rapide lorsque le pool de threads a injecté suffisamment de threads. Si nous avions configuré le pool de threads au préalable (via SetMinThreads), il aurait surpassé l’async dès le départ.
Et la consommation de ressources, me demanderez-vous ? “Un thread a un coût important en termes de temps CPU pour l’ordonnancement, le changement de contexte et l’empreinte RAM”. Pas si vite. L’ordonnancement et le changement de contexte des threads sont efficaces. En ce qui concerne l’utilisation de la pile, le thread ne consomme pas instantanément la RAM mais réserve simplement l’espace d’adressage virtuel et n’alloue qu’une petite fraction de ce qui est réellement nécessaire.
Regardons ce que disent les données. Même avec un plus grand nombre de threads, la version synchrone a une empreinte mémoire plus faible (working set qui correspond à la mémoire physique).
MISE À JOUR. Je souhaite publier les résultats de l’expérience complémentaire qui devrait être plus représentative car elle évite certains biais de la première.
Tout d’abord, les résultats de la première expérience sont obtenus avec IIS Express, qui est essentiellement un serveur de développement, j’ai donc dû m’en éloigner. De plus, en tenant compte des retours, j’ai isolé la machine de génération de charge du serveur (deux VM Azure dans le même réseau). J’ai également découvert que certaines limites de threading IIS sont difficiles voire impossibles à contourner et j’ai fini par passer à l’auto-hébergement ASP.NET WebAPI pour éliminer IIS des variables. Notez que les empreintes mémoire/temps CPU sont radicalement différents avec ce test, veuillez ne pas comparer les chiffres entre les différentes exécutions de test car les configurations sont totalement différentes (hébergement, matériel, configuration des machines). De plus, quand je suis passé sur d’autres machines et une autre solution d’hébergement, la stratégie du pool de threads a changé (elle est dynamique) et le taux d’injection a augmenté.
Paramètres : délai 100 ms, 200 “utilisateurs” JMeter, temps de montée en charge de 30 secondes.
Je souhaite conclure ces expériences par ce qui suit : oui, dans certaines circonstances spécifiques (plutôt de type laboratoire), il est possible d’obtenir des résultats comparables entre synchrone et asynchrone, mais dans les cas réels où la charge de travail ne peut pas être prévisible à 100 % et où la charge est inégale, nous finirons inévitablement par atteindre une sorte de limite de threading : soit des limites côté serveur, soit des limites de croissance du pool de threads (et gardez à l’esprit que la gestion du pool de threads est un mécanisme automatique dont les propriétés ne sont pas toujours facilement prévisibles). De plus, la version synchrone a effectivement une empreinte mémoire plus importante (à la fois le working set et une taille de mémoire virtuelle bien plus grande). En ce qui concerne la consommation CPU, l’asynchrone gagne également (métrique du temps CPU par requête).
Sur IIS avec les paramètres par défaut, la situation est encore plus dramatique : la version synchrone est d’un ou plusieurs ordres de grandeur plus lente (et un débit moindre) en raison de la limite assez stricte du nombre de threads - 20 par CPU.
PS. Utilisez les pipelines asynchrones pour les E/S ! [… soupir de soulagement…]