Pendant les Techdays ici aux Pays-Bas, Steve Sanderson a fait une présentation sur C # 5, ASP.NET MVC 4 et le Web asynchrone.
Il a expliqué que lorsque les requêtes mettent du temps à se terminer, tous les threads du pool de threads deviennent occupés et les nouvelles requêtes doivent attendre. Le serveur ne peut pas gérer la charge et tout ralentit.
Il a ensuite montré comment l'utilisation des webrequests asynchrones améliore les performances car le travail est ensuite délégué à un autre thread et le pool de threads peut répondre rapidement aux nouvelles demandes entrantes. Il a même fait une démonstration de cela et a montré que 50 demandes simultanées prenaient d'abord 50 * 1 mais avec le comportement asynchrone en place seulement 1,2 s au total.
Mais après avoir vu cela, j'ai encore quelques questions.
Pourquoi ne pouvons-nous pas simplement utiliser un plus grand pool de threads? N'est-ce pas en utilisant async/wait pour faire apparaître un autre thread plus lentement que d'augmenter simplement le pool de threads depuis le début? Ce n'est pas comme si le serveur sur lequel nous fonctionnions obtenait soudainement plus de threads ou quelque chose?
La demande de l'utilisateur attend toujours la fin du thread asynchrone. Si le thread du pool fait autre chose, comment le thread 'UI' est-il occupé? Steve a parlé d'un "noyau intelligent qui sait quand quelque chose est terminé". Comment ça marche?
C'est une très bonne question, et comprendre qu'il est essentiel de comprendre pourquoi asynchrone IO est si important. La raison pour laquelle la nouvelle fonctionnalité asynchrone/attente a été ajoutée à C # 5.0 est de simplifier l'écriture asynchrone La prise en charge du traitement asynchrone sur le serveur n'est pas nouvelle cependant, elle existe depuis ASP.NET 2.0.
Comme Steve vous l'a montré, avec le traitement synchrone, chaque demande dans ASP.NET (et WCF) prend un thread du pool de threads. Le problème qu'il a présenté est un problème bien connu appelé " famine du pool de threads ". Si vous rendez synchrone IO sur votre serveur, le thread du pool de threads restera bloqué (ne faisant rien) pendant la durée de l'IO. Puisqu'il y a une limite dans le nombre de threads dans le pool de threads , sous charge, cela peut entraîner une situation dans laquelle tous les threads du pool de threads sont bloqués en attente d'E/S et les demandes commencent à être mises en file d'attente, ce qui entraîne une augmentation du temps de réponse. Comme tous les threads attendent un IO pour terminer, vous verrez une occupation CPU proche de 0% (même si les temps de réponse passent par le toit).
Ce que vous demandez ( Pourquoi ne pouvons-nous pas simplement utiliser un plus grand pool de threads? ) est une très bonne question. En fait, c'est ainsi que la plupart des gens ont résolu le problème de la famine du pool de threads jusqu'à présent: il suffit d'avoir plus de threads sur le pool de threads. Certaines documentations de Microsoft indiquent même que comme solution pour les situations où la famine du pool de threads peut se produire. C'est une solution acceptable, et jusqu'à C # 5.0, c'était beaucoup plus facile à faire que de réécrire votre code pour qu'il soit complètement asynchrone.
Il y a cependant quelques problèmes avec l'approche:
Aucune valeur ne fonctionne dans toutes les situations : le nombre de threads du pool de threads dont vous aurez besoin dépend linéairement de la durée de l'IO, et charger sur votre serveur. Malheureusement, la latence IO est généralement imprévisible. Voici un exemple: supposons que vous fassiez des requêtes HTTP à un service Web tiers dans votre application ASP.NET, ce qui prend environ 2 secondes. rencontrez la famine du pool de threads, vous décidez donc d'augmenter la taille du pool de threads à, disons, 200 threads, puis cela recommence à fonctionner correctement. Le problème est que la semaine prochaine, le service Web rencontrera des problèmes techniques qui augmenteront son temps de réponse à 10 secondes. Tout à coup, la famine du pool de threads est de retour, car les threads sont bloqués 5 fois plus longtemps, vous devez donc augmenter le nombre 5 fois, à 1 000 threads.
Évolutivité et performances : Le deuxième problème est que si vous faites cela, vous utiliserez toujours un thread par demande. Les threads sont une ressource coûteuse. Chaque thread géré dans .NET nécessite une allocation de mémoire de 1 Mo pour la pile. Pour une page Web faisant IO qui durent 5 secondes, et avec une charge de 500 requêtes par seconde, vous aurez besoin de 2500 threads dans votre pool de threads, ce qui signifie 2,5 Go de mémoire pour les piles de threads qui ne fera rien. Ensuite, vous avez la question du changement de contexte, qui aura un lourd tribut sur les performances de votre machine (affectant tous les services sur la machine, pas seulement votre application Web). Même si Windows fait un assez bon pour ignorer les threads en attente, il n'est pas conçu pour gérer un si grand nombre de threads. N'oubliez pas que l'efficacité la plus élevée est obtenue lorsque le nombre de threads en cours d'exécution est égal au nombre de CPU logiques sur la machine (généralement pas plus de 16).
Augmenter la taille du pool de threads est donc une solution, et les gens le font depuis une décennie (même dans les propres produits de Microsoft), il est juste moins évolutif et efficace, en termes de mémoire et d'utilisation du processeur, et vous êtes toujours à la merci d'une augmentation soudaine de IO latence qui provoquerait la famine. Jusqu'à C # 5.0, la complexité du code asynchrone ne valait pas la peine pour beaucoup de gens. async/wait change tout comme maintenant , vous pouvez bénéficier de l'évolutivité des E/S asynchrones et écrire du code simple en même temps.
Plus de détails: http://msdn.Microsoft.com/en-us/library/ff647787.aspx " Utilisez des appels asynchrones pour appeler des services Web ou des objets distants lorsqu'il est possible de effectuer un traitement parallèle supplémentaire pendant le déroulement de l'appel de service Web. Dans la mesure du possible, évitez les appels synchrones (blocage) aux services Web car les appels de service Web sortants sont effectués à l'aide de threads du pool de threads ASP.NET. Les appels de blocage réduisent le nombre de threads disponibles pour traitement d'autres demandes entrantes. "
SynchronizationContext
. Vous pouvez en savoir plus sur SynchronizationContext
dans mon article MSDN - il explique comment fonctionne SynchronizationContext
d'ASP.NET et comment await
utilise SynchronizationContext
.Le traitement asynchrone ASP.NET était possible avant async/wait - vous pouviez utiliser des pages asynchrones et utiliser des composants EAP tels que WebClient
(La programmation asynchrone basée sur les événements est un style de programmation asynchrone basé sur SynchronizationContext
). Async/Wait utilise également SynchronizationContext
, mais a une syntaxe beaucoup plus facile .
Imaginez le pool de threads comme un ensemble de travailleurs que vous avez employés pour faire votre travail. Vos employés exécutent rapidement cp les instructions pour votre code .
Maintenant, votre travail dépend du travail d'un autre type lent; le gars lent étant le disque ou le résea. Par exemple, votre travail peut avoir deux parties, une partie qui doit exécuter avant le travail du gars lent, et une partie qui doit exécuter après le travail du gars lent.
Comment conseilleriez-vous à vos employés de faire votre travail? Diriez-vous à chaque travailleur - "Faites cette première partie, puis attendez que ce type lent soit fait, puis faites votre deuxième partie"? Souhaitez-vous augmenter le nombre de vos employés parce qu'ils semblent tous attendre ce type lent et que vous n'êtes pas en mesure de satisfaire de nouveaux clients? Non!
Au lieu de cela, vous demanderiez à chaque travailleur de faire la première partie et demanderiez au type lent de revenir et de déposer un message dans une file d'attente une fois terminé. Vous diriez à chaque travailleur (ou peut-être un sous-ensemble dédié de travailleurs) de rechercher les messages terminés dans la file d'attente et d'effectuer la deuxième partie du travail.
Le noyau intelligent auquel vous faites allusion ci-dessus est la capacité des systèmes d'exploitation à maintenir une telle file d'attente pour le disque lent et le réseau IO messages d'achèvement.