Après cette question, cela me met à l'aise lors de l'utilisation d'opérations asynchrones dans ASP.NET MVC. J'ai donc écrit deux articles de blog à ce sujet:
J'ai trop de malentendus dans mon esprit sur les opérations asynchrones sur ASP.NET MVC.
J'entends toujours cette phrase: l' application peut mieux évoluer si les opérations s'exécutent de manière asynchrone
Et j'ai aussi beaucoup entendu ce genre de phrases: si vous avez un volume de trafic énorme, vous feriez peut-être mieux de ne pas effectuer vos requêtes de manière asynchrone - consommer 2 threads supplémentaires pour traiter une demande enlève des ressources aux autres demandes entrantes.
Je pense que ces deux phrases sont incohérentes.
Je n'ai pas beaucoup d'informations sur le fonctionnement de threadpool sur ASP.NET mais je sais que threadpool a une taille limitée pour les threads. Donc, la deuxième phrase doit être liée à cette question.
Et je voudrais savoir si les opérations asynchrones dans ASP.NET MVC utilisent un thread de ThreadPool sur .NET 4?
Par exemple, lorsque nous implémentons un AsyncController, comment se structure l'application? Si j'obtiens un trafic énorme, est-ce une bonne idée d'implémenter AsyncController?
Y a-t-il quelqu'un là-bas qui peut enlever ce rideau noir sous mes yeux et m'expliquer l'accord sur l'asynchronie sur ASP.NET MVC 3 (NET 4)?
Éditer:
J'ai lu ce document ci-dessous près de centaines de fois et je comprends le problème principal, mais j'ai toujours de la confusion car il y a trop de commentaires incohérents.
Utilisation d'un contrôleur asynchrone dans ASP.NET MVC
Éditer:
Supposons que j'ai une action de contrôleur comme ci-dessous (pas une implémentation de AsyncController
cependant):
public ViewResult Index() {
Task.Factory.StartNew(() => {
//Do an advanced looging here which takes a while
});
return View();
}
Comme vous le voyez ici, je lance une opération et l'oublie. Ensuite, je rentre tout de suite sans attendre qu'il soit terminé.
Dans ce cas, cela doit-il utiliser un thread de threadpool? Si tel est le cas, une fois terminé, qu'advient-il de ce fil? Est-ce qu'il GC
entre et nettoie juste après la fin?
Éditer:
Pour la réponse de @ Darin, voici un exemple de code asynchrone qui communique avec la base de données:
public class FooController : AsyncController {
//EF 4.2 DbContext instance
MyContext _context = new MyContext();
public void IndexAsync() {
AsyncManager.OutstandingOperations.Increment(3);
Task<IEnumerable<Foo>>.Factory.StartNew(() => {
return
_context.Foos;
}).ContinueWith(t => {
AsyncManager.Parameters["foos"] = t.Result;
AsyncManager.OutstandingOperations.Decrement();
});
Task<IEnumerable<Bars>>.Factory.StartNew(() => {
return
_context.Bars;
}).ContinueWith(t => {
AsyncManager.Parameters["bars"] = t.Result;
AsyncManager.OutstandingOperations.Decrement();
});
Task<IEnumerable<FooBar>>.Factory.StartNew(() => {
return
_context.FooBars;
}).ContinueWith(t => {
AsyncManager.Parameters["foobars"] = t.Result;
AsyncManager.OutstandingOperations.Decrement();
});
}
public ViewResult IndexCompleted(
IEnumerable<Foo> foos,
IEnumerable<Bar> bars,
IEnumerable<FooBar> foobars) {
//Do the regular stuff and return
}
}
la source
Réponses:
Voici un excellent article que je vous recommanderais de lire pour mieux comprendre le traitement asynchrone dans ASP.NET (ce que représentent essentiellement les contrôleurs asynchrones).
Considérons d'abord une action synchrone standard:
Lorsqu'une demande est faite à cette action, un thread est tiré du pool de threads et le corps de cette action est exécuté sur ce thread. Donc, si le traitement à l'intérieur de cette action est lent, vous bloquez ce thread pendant tout le traitement, ce thread ne peut donc pas être réutilisé pour traiter d'autres requêtes. À la fin de l'exécution de la requête, le thread est renvoyé au pool de threads.
Prenons maintenant un exemple du modèle asynchrone:
Lorsqu'une requête est envoyée à l'action Index, un thread est tiré du pool de threads et le corps de la
IndexAsync
méthode est exécuté. Une fois l'exécution du corps de cette méthode terminée, le thread est renvoyé dans le pool de threads. Ensuite, en utilisant la normeAsyncManager.OutstandingOperations
, une fois que vous avez signalé la fin de l'opération asynchrone, un autre thread est tiré du pool de threads et le corps de l'IndexCompleted
action est exécuté sur celui-ci et le résultat est rendu au client.Donc, ce que nous pouvons voir dans ce modèle, c'est qu'une seule requête HTTP client pourrait être exécutée par deux threads différents.
Maintenant, la partie intéressante se passe à l'intérieur de la
IndexAsync
méthode. Si vous avez une opération de blocage à l'intérieur, vous gaspillez totalement le but des contrôleurs asynchrones parce que vous bloquez le thread de travail (rappelez-vous que le corps de cette action est exécuté sur un thread tiré du pool de threads).Alors, quand pouvons-nous vraiment tirer parti des contrôleurs asynchrones que vous pourriez demander?
À mon humble avis, nous pouvons gagner le plus lorsque nous avons des opérations intensives d'E / S (telles que la base de données et les appels réseau vers des services distants). Si vous avez une opération intensive en CPU, les actions asynchrones ne vous apporteront pas beaucoup d'avantages.
Alors, pourquoi pouvons-nous tirer parti des opérations intensives d'E / S? Parce que nous pourrions utiliser les ports d' achèvement d' E / S . Les IOCP sont extrêmement puissants car vous ne consommez pas de threads ou de ressources sur le serveur pendant l'exécution de toute l'opération.
Comment travaillent-ils?
Supposons que nous souhaitons télécharger le contenu d'une page Web distante à l'aide de la méthode WebClient.DownloadStringAsync . Vous appelez cette méthode qui enregistrera un IOCP dans le système d'exploitation et reviendra immédiatement. Pendant le traitement de l'ensemble de la requête, aucun thread n'est consommé sur votre serveur. Tout se passe sur le serveur distant. Cela peut prendre beaucoup de temps, mais cela ne vous dérange pas car vous ne mettez pas en péril vos threads de travail. Une fois qu'une réponse est reçue, l'IOCP est signalé, un thread est tiré du pool de threads et le rappel est exécuté sur ce thread. Mais comme vous pouvez le voir, pendant tout le processus, nous n'avons monopolisé aucun thread.
Il en va de même avec des méthodes telles que FileStream.BeginRead, SqlCommand.BeginExecute, ...
Qu'en est-il de la parallélisation de plusieurs appels de base de données? Supposons que vous ayez une action de contrôleur synchrone dans laquelle vous avez effectué 4 appels de base de données bloquants en séquence. Il est facile de calculer que si chaque appel de base de données prend 200 ms, l'action de votre contrôleur prendra environ 800 ms pour s'exécuter.
Si vous n'avez pas besoin d'exécuter ces appels de manière séquentielle, leur parallélisation améliorerait-elle les performances?
C'est la grande question à laquelle il n'est pas facile de répondre. Peut-être que oui, peut-être que non. Cela dépendra entièrement de la façon dont vous implémentez ces appels de base de données. Si vous utilisez des contrôleurs asynchrones et des ports d'achèvement d'E / S comme indiqué précédemment, vous améliorerez les performances de cette action de contrôleur et d'autres actions, car vous ne monopoliserez pas les threads de travail.
En revanche, si vous les implémentez mal (avec un appel de base de données bloquant effectué sur un thread du pool de threads), vous réduirez fondamentalement le temps total d'exécution de cette action à environ 200 ms mais vous auriez consommé 4 threads de travail donc vous peut avoir dégradé les performances d'autres requêtes qui peuvent devenir affamées en raison de threads manquants dans le pool pour les traiter.
C'est donc très difficile et si vous ne vous sentez pas prêt à effectuer des tests approfondis sur votre application, n'implémentez pas de contrôleurs asynchrones, car il y a de fortes chances que vous fassiez plus de dégâts que de bénéfices. Ne les implémentez que si vous avez une raison de le faire: par exemple, vous avez identifié que les actions standard du contrôleur synchrone constituent un goulot d'étranglement pour votre application (après avoir effectué des tests de charge et des mesures approfondis bien sûr).
Considérons maintenant votre exemple:
Lorsqu'une demande est reçue pour l'action Index, un thread est tiré du pool de threads pour exécuter son corps, mais son corps ne planifie qu'une nouvelle tâche à l'aide de TPL . Ainsi, l'exécution de l'action se termine et le thread est renvoyé dans le pool de threads. Sauf que, TPL utilise des threads du pool de threads pour effectuer leur traitement. Ainsi, même si le thread d'origine a été renvoyé dans le pool de threads, vous avez dessiné un autre thread de ce pool pour exécuter le corps de la tâche. Vous avez donc mis en péril 2 threads de votre précieux pool.
Considérons maintenant ce qui suit:
Dans ce cas, nous créons manuellement un thread. Dans ce cas, l'exécution du corps de l'action Index peut prendre un peu plus de temps (car générer un nouveau thread est plus coûteux que d'en tirer un à partir d'un pool existant). Mais l'exécution de l'opération de journalisation avancée se fera sur un thread qui ne fait pas partie du pool. Nous ne mettons donc pas en péril les threads du pool qui restent libres pour répondre à une autre requête.
la source
System.Threading.Task
) exécutées dans laIndexAsync
méthode. À l'intérieur de ces opérations, nous faisons des appels db à un serveur. Donc, toutes sont des opérations intensives d'E / S, non? Dans ce cas, créons-nous 4 threads séparés (ou obtient-on 4 threads séparés du pool de threads)? En supposant que j'ai une machine multicœur, ils vont également fonctionner en parallèle, non?SqlCommand.ExecuteReader
vous gaspillez tout car il s'agit d'un appel bloquant. Vous bloquez le thread sur lequel cet appel s'exécute et si ce thread se trouve être un thread du pool, c'est très mauvais. Vous bénéficierez uniquement si vous utilisez E / S Ports d' achèvement:SqlCommand.BeginExecuteReader
. Si vous n'utilisez pas IOCP quoi que vous fassiez, n'utilisez pas de contrôleurs asynchrones car vous ferez plus de dégâts que de bénéfices sur les performances globales de votre application._context.Foo
vous n'exécutez rien. Vous construisez juste un arbre d'expression. Soyez extrêmement prudent avec cela. L'exécution de la requête est différée uniquement lorsque vous commencez à énumérer le jeu de résultats. Et si cela se produit dans la vue, cela peut être catastrophique pour les performances. Pour exécuter avec empressement une requête EF, ajoutez.ToList()
à la fin.Oui - tous les threads proviennent du pool de threads. Votre application MVC est déjà multithread, lorsqu'une demande arrive dans un nouveau thread sera extrait du pool et utilisé pour traiter la demande. Ce thread sera «verrouillé» (à partir d'autres demandes) jusqu'à ce que la demande soit entièrement traitée et terminée. S'il n'y a pas de thread disponible dans le pool, la demande devra attendre jusqu'à ce qu'il soit disponible.
Si vous avez des contrôleurs asynchrones, ils obtiennent toujours un thread du pool mais tout en traitant la demande, ils peuvent abandonner le thread, en attendant que quelque chose se passe (et ce thread peut être donné à une autre demande) et lorsque la demande d'origine a besoin d'un thread encore une fois, il en obtient un de la piscine.
La différence est que si vous avez beaucoup de requêtes de longue durée (où le thread attend une réponse de quelque chose), vous risquez de manquer de threads du pool pour répondre même aux requêtes de base. Si vous avez des contrôleurs asynchrones, vous n'avez plus de threads mais les threads en attente sont renvoyés dans le pool et peuvent traiter d'autres requêtes.
Un exemple presque réel ... Pensez-y comme si vous montiez dans un bus, il y a cinq personnes qui attendent pour monter, le premier monte, paie et s'assoit (le chauffeur a répondu à sa demande), vous montez (le chauffeur fait l'entretien votre demande) mais vous ne trouvez pas votre argent; lorsque vous fouillez dans vos poches, le chauffeur vous abandonne et engage les deux personnes suivantes (répondant à leurs demandes), lorsque vous trouvez votre argent, le chauffeur recommence à traiter avec vous (complétant votre demande) - la cinquième personne doit attendre jusqu'à vous avez terminé, mais les troisième et quatrième personnes ont été servies alors que vous en étiez à mi-chemin. Cela signifie que le conducteur est le seul et unique fil conducteur de la piscine et les passagers sont les demandes. C'était trop compliqué d'écrire comment cela fonctionnerait s'il y avait deux pilotes mais vous pouvez imaginer ...
Sans un contrôleur asynchrone, les passagers derrière vous devraient attendre longtemps pendant que vous cherchez votre argent, tandis que le chauffeur de bus ne ferait aucun travail.
Donc, la conclusion est que si beaucoup de gens ne savent pas où se trouve leur argent (c'est-à-dire qu'ils ont besoin de beaucoup de temps pour répondre à quelque chose que le pilote a demandé), les contrôleurs asynchrones pourraient bien aider à traiter les requêtes, en accélérant le processus de certains. Sans contrôleur aysnc, tout le monde attend que la personne en face soit complètement traitée. MAIS n'oubliez pas que dans MVC vous avez beaucoup de pilotes de bus sur un seul bus, donc l'async n'est pas un choix automatique.
la source
Il y a deux concepts en jeu ici. Tout d'abord, nous pouvons faire fonctionner notre code en parallèle pour exécuter plus rapidement ou programmer du code sur un autre thread pour éviter de faire attendre l'utilisateur. L'exemple que vous aviez
appartient à la deuxième catégorie. L'utilisateur obtiendra une réponse plus rapide mais la charge de travail totale sur le serveur est plus élevée car il doit faire le même travail + gérer le threading.
Un autre exemple de ceci serait:
Étant donné que les requêtes s'exécutent en parallèle, l'utilisateur n'aura pas à attendre aussi longtemps que si elles étaient effectuées en série. Mais vous devez comprendre que nous utilisons plus de ressources ici que si nous exécutions en série parce que nous exécutons le code sur de nombreux threads pendant que nous avons également un thread en attente.
C'est parfaitement bien dans un scénario client. Et il est assez courant d'envelopper du code synchrone de longue durée dans une nouvelle tâche (l'exécuter sur un autre thread), ainsi que de garder l'interface utilisateur réactive ou de la mettre en parallèle pour la rendre plus rapide. Un fil est toujours utilisé pendant toute la durée. Sur un serveur avec une charge élevée, cela pourrait se retourner contre vous car vous utilisez en fait plus de ressources. C'est ce dont les gens vous ont mis en garde
Les contrôleurs async dans MVC ont cependant un autre objectif. Le but ici est d'éviter d'avoir des threads assis autour de ne rien faire (qui peut nuire à l'évolutivité). Cela n'a vraiment d'importance que si les API que vous appelez ont des méthodes asynchrones. Comme WebClient.DowloadStringAsync ().
Le fait est que vous pouvez laisser votre thread être renvoyé pour gérer les nouvelles demandes jusqu'à ce que la demande Web soit terminée, où il vous appellera un rappel qui obtient le même thread ou un nouveau thread et termine la demande.
J'espère que vous comprenez la différence entre asynchrone et parallèle. Considérez le code parallèle comme du code dans lequel votre thread se trouve et attendez le résultat. Alors que le code asynchrone est un code dans lequel vous serez averti lorsque le code est terminé et que vous pourrez recommencer à travailler dessus, en attendant, le thread peut faire d'autres travaux.
la source
Les applications peuvent mieux évoluer si les opérations s'exécutent de manière asynchrone, mais uniquement si des ressources sont disponibles pour traiter les opérations supplémentaires .
Les opérations asynchrones garantissent que vous ne bloquez jamais une action car une action existante est en cours. ASP.NET a un modèle asynchrone qui permet à plusieurs demandes de s'exécuter côte à côte. Il serait possible de mettre les demandes en file d'attente et de les traiter FIFO, mais cela ne serait pas bien mis à l'échelle lorsque vous avez des centaines de demandes en file d'attente et que chaque demande prend 100 ms à traiter.
Si vous avez un volume de trafic énorme, il vaut peut- être mieux ne pas effectuer vos requêtes de manière asynchrone, car il se peut qu'il n'y ait pas de ressources supplémentaires pour traiter les requêtes . S'il n'y a pas de ressources de rechange, vos demandes sont obligées de faire la queue, prennent plus de temps ou échouent carrément, auquel cas la surcharge asynchrone (mutex et opérations de changement de contexte) ne vous donne rien.
En ce qui concerne ASP.NET, vous n'avez pas le choix - il utilise un modèle asynchrone, car c'est ce qui a du sens pour le modèle serveur-client. Si vous deviez écrire votre propre code en interne qui utilise un modèle asynchrone pour tenter de mieux évoluer, à moins que vous n'essayiez de gérer une ressource partagée entre toutes les demandes, vous ne verrez aucune amélioration car elles sont déjà encapsulées dans un processus asynchrone qui ne bloque rien d'autre.
En fin de compte, tout cela est subjectif jusqu'à ce que vous regardiez réellement ce qui cause un goulot d'étranglement dans votre système. Parfois, il est évident qu'un modèle asynchrone aidera (en empêchant un blocage des ressources en file d'attente). En fin de compte, seules la mesure et l'analyse d'un système peuvent indiquer où vous pouvez gagner en efficacité.
Éditer:
Dans votre exemple, l'
Task.Factory.StartNew
appel mettra en file d'attente une opération sur le pool de threads .NET. La nature des threads du pool de threads doit être réutilisée (pour éviter le coût de création / destruction de nombreux threads). Une fois l'opération terminée, le thread est relâché dans le pool pour être réutilisé par une autre demande (le garbage collector n'intervient pas réellement à moins que vous ayez créé des objets dans vos opérations, auquel cas ils sont collectés normalement. cadrage).En ce qui concerne ASP.NET, il n'y a pas d'opération spéciale ici. La demande ASP.NET se termine sans égard à la tâche asynchrone. Le seul problème peut être si votre pool de threads est saturé (c'est-à-dire qu'il n'y a pas de threads disponibles pour traiter la demande pour le moment et les paramètres du pool ne permettent pas de créer plus de threads), auquel cas la requête est bloquée en attendant de démarrer le tâche jusqu'à ce qu'un thread de pool devienne disponible.
la source
Task.Factory.StartNew
appel mettra en file d'attente une opération sur le pool de threads .NET. . Dans ce contexte, lequel est correct ici: 1-) Il crée un nouveau thread et quand c'est fait, ce thread retourne à threadpool et attend là pour être réutilisé à nouveau. 2-) Il obtient un thread de threadpool et ce thread retourne à threadpool et attend là pour être réutilisé à nouveau. 3-) Il adopte l'approche la plus efficace et peut faire l'un ou l'autre.Oui, ils utilisent un thread du pool de threads. Il existe en fait un excellent guide de MSDN qui répondra à toutes vos questions et plus encore. Je l'ai trouvé très utile dans le passé. Vérifiez-le!
http://msdn.microsoft.com/en-us/library/ee728598.aspx
Pendant ce temps, les commentaires + suggestions que vous entendez sur le code asynchrone doivent être pris avec un grain de sel. Pour commencer, le simple fait de rendre quelque chose asynchrone ne permet pas nécessairement de mieux évoluer et, dans certains cas, peut aggraver la mise à l'échelle de votre application. L'autre commentaire que vous avez publié sur "un énorme volume de trafic ..." n'est également correct que dans certains contextes. Cela dépend vraiment de ce que font vos opérations et de la manière dont elles interagissent avec les autres parties du système.
En bref, beaucoup de gens ont beaucoup d'opinions sur l'async, mais elles peuvent ne pas être correctes hors de leur contexte. Je dirais que vous vous concentrez sur vos problèmes exacts et que vous effectuez des tests de performances de base pour voir quels contrôleurs asynchrones, etc. gèrent réellement avec votre application.
la source
GC
vient les nettoyer juste après que j'ai terminé afin que mon application n'ait pas de fuite de mémoire, ainsi de suite. Toute idée sur cette partie.Tout d'abord, ce n'est pas MVC mais l'IIS qui gère le pool de threads. Ainsi, toute demande qui vient à l'application MVC ou ASP.NET est servie à partir de threads qui sont maintenus dans le pool de threads. Seulement en rendant l'application Asynch, il invoque cette action dans un thread différent et libère le thread immédiatement afin que d'autres requêtes puissent être prises.
J'ai expliqué la même chose avec une vidéo détaillée ( http://www.youtube.com/watch?v=wvg13n5V0V0/ "MVC Asynch controllers and thread starvation") qui montre comment la famine de thread se produit dans MVC et comment elle est minimisée en utilisant MVC Contrôleurs asynchrones J'ai également mesuré les files d'attente de demandes à l'aide de perfmon afin que vous puissiez voir comment les files d'attente de demandes sont réduites pour l'asynchrone MVC et son pire pour les opérations de synchronisation.
la source