web-dev-qa-db-fra.com

Effectuer des opérations asynchrones dans ASP.NET MVC utilise un thread de ThreadPool sur .NET 4

Après cette question, je suis à l'aise lorsque j'utilise des opérations asynchrones dans ASP.NET MVC. J'ai donc écrit deux articles sur ce blog:

J'ai trop de malentendus dans mon esprit à propos des opérations asynchrones sur ASP.NET MVC.

J'entends toujours cette phrase: L'application peut mieux s'adapter 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, il est peut-être préférable de ne pas exécuter vos requêtes de manière asynchrone - 2 Des threads supplémentaires pour traiter une requête enlèvent des ressources aux autres requêtes 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. La deuxième phrase doit donc être liée à cette question.

Et j'aimerais 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 l’application se structure-t-elle? Si le trafic est énorme, est-ce une bonne idée d'implémenter AsyncController?

Y a-t-il quelqu'un là-bas qui peut me prendre ce rideau noir devant mes yeux et m'expliquer le problème concernant l'asynchronisme sur ASP.NET MVC 3 (NET 4)?

Modifier:

J'ai lu le document ci-dessous presque des centaines de fois et je comprends l'accord principal, mais j'ai toujours de la confusion parce qu'il y a trop de commentaires incohérents.

tilisation d'un contrôleur asynchrone dans ASP.NET MVC

Modifier:

Supposons que j'ai une action de contrôleur comme ci-dessous (pas une implémentation de AsyncController bien que):

public ViewResult Index() { 

    Task.Factory.StartNew(() => { 
        //Do an advanced looging here which takes a while
    });

    return View();
}

Comme vous le voyez ici, je déclenche une opération et l’oublie. Ensuite, je reviens immédiatement sans attendre qu'elle soit terminée.

Dans ce cas, cela doit-il utiliser un thread de threadpool? Si c'est le cas, une fois l'opération terminée, qu'advient-il de ce fil? Est-ce que GC entre et nettoie juste après la fin?

Modifier:

Pour la réponse de @ Darin, voici un exemple de code asynchrone qui parle à 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

    }
}
157
tugberk

Voici un excellent article Je vous recommande de lire pour mieux comprendre le traitement asynchrone dans ASP.NET (ce que représentent en réalité les contrôleurs asynchrones).

Considérons d’abord une action synchrone standard:

public ActionResult Index()
{
    // some processing
    return View();
}

Lorsqu'une demande est faite pour cette action, un thread est tiré du pool de threads et le corps de cette action est exécuté sur ce thread. Ainsi, si le traitement à l'intérieur de cette action est lent, vous bloquez ce thread pour tout le traitement. Ce thread ne peut donc pas être réutilisé pour traiter d'autres demandes. À la fin de l'exécution de la demande, le thread est renvoyé au pool de threads.

Prenons maintenant un exemple du motif asynchrone:

public void IndexAsync()
{
    // perform some processing
}

public ActionResult IndexCompleted(object result)
{
    return View();
}

Lorsqu'une demande est envoyée à l'action Index, un thread est tiré du pool de threads et le corps de la méthode IndexAsync est exécuté. Une fois que le corps de cette méthode a fini de s'exécuter, le thread est renvoyé au pool de threads. Puis, à l'aide du AsyncManager.OutstandingOperations standard, une fois que vous avez signalé l'achèvement de l'opération asynchrone, un autre thread est tiré du pool de threads et le corps de l'action IndexCompleted est exécuté et le résultat est renvoyé au client.

Nous voyons donc dans ce modèle qu'une requête HTTP client unique peut être exécutée par deux threads différents.

Maintenant, la partie intéressante se passe dans la méthode IndexAsync. Si vous avez une opération de blocage à l'intérieur, vous gâchez tout le but des contrôleurs asynchrones, car vous bloquez le thread de travail (rappelez-vous que le corps de cette action est exécuté sur un thread extrait du pool de threads).

Alors, quand pouvons-nous réellement tirer parti des contrôleurs asynchrones que vous pourriez vous demander?

IMHO nous pouvons obtenir le plus lorsque nous avons des opérations intensives d'E/S (telles que des appels de base de données et réseau vers des services distants). Si vous utilisez beaucoup de ressources processeur, les actions asynchrones ne vous apporteront pas beaucoup d'avantages.

Alors, pourquoi pouvons-nous tirer profit des opérations intensives en E/S? Parce que nous pourrions utiliser Ports de complétion 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 voulions 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 la demande entière, 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 les threads de vos ouvriers. Une fois la réponse reçue, le protocole 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 constater, pendant tout le processus, nous n’avons monopolisé aucun fil.

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 eu une action de contrôleur synchrone dans laquelle vous avez bloqué 4 appels de base de données 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 nécessite environ 800 ms.

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épend entièrement de la manière dont vous implémentez ces appels de base de données. Si vous utilisez des contrôleurs asynchrones et des ports de complétion 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.

D'autre part, 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 le temps total d'exécution de cette action à environ 200 ms, mais vous auriez consommé 4 threads de travail. peut avoir dégradé les performances d'autres requêtes qui pourraient devenir affamées à cause 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'utilisez pas de contrôleurs asynchrones, car vous risquez de causer plus de dégâts que d'avantages. Ne les implémentez que si vous avez une raison de le faire: par exemple, vous avez identifié le fait que les actions d'un contrôleur synchrone standard 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:

public ViewResult Index() { 

    Task.Factory.StartNew(() => { 
        //Do an advanced looging here which takes a while
    });

    return View();
}

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 programme qu'une nouvelle tâche à l'aide de TPL . Ainsi, l'exécution de l'action se termine et le thread est renvoyé au pool de threads. Sauf que, TPL utilise les threads du pool de threads pour effectuer leur traitement. Ainsi, même si le thread d'origine a été renvoyé au 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 fils de votre précieux bassin.

Considérons maintenant ce qui suit:

public ViewResult Index() { 

    new Thread(() => { 
        //Do an advanced looging here which takes a while
    }).Start();

    return View();
}

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 longtemps (car créer un nouveau thread coûte plus cher que d'en tirer un depuis un pool existant). Mais l'exécution de l'opération de journalisation avancée sera effectuée sur un thread qui ne fait pas partie du pool. Nous ne compromettons donc pas les threads du pool qui restent libres pour répondre à d'autres requêtes.

175
Darin Dimitrov

Oui, tous les threads proviennent du pool de threads. Votre application MVC est déjà multithread. Lorsqu'une demande est présentée dans un nouveau thread, elle est extraite du pool et utilisée pour répondre à la demande. Ce thread sera "verrouillé" (à partir d'autres requêtes) jusqu'à ce que la demande soit entièrement traitée et complété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 répondant à la demande, ils peuvent abandonner le thread, en attendant que quelque chose se produise (et que ce thread puisse être attribué à une autre demande) et lorsque la demande d'origine nécessite un thread. à nouveau, il en reçoit 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), vous risquez de manquer de threads du pool pour traiter même les requêtes de base. Si vous avez des contrôleurs asynchrones, vous n'avez plus de threads, mais ceux qui sont en attente sont renvoyés au pool et peuvent traiter d'autres demandes.

Un presque exemple réel ... Pensez-y comme si vous montiez dans un bus, cinq personnes attendent de monter, le premier monte, paie et s'assied (le chauffeur a répondu à leur demande), vous montez (le chauffeur répond à votre demande) mais vous ne trouvez pas votre argent; lorsque vous fouillez dans vos poches, le chauffeur vous abandonne et met les deux personnes suivantes en contact (demande de service), lorsque vous retrouvez votre argent, le chauffeur recommence à traiter avec vous (finalisant votre demande) - la cinquième personne doit attendre jusqu'à ce que vous avez terminé, mais les troisième et quatrième personnes ont été servies alors que vous étiez à mi-chemin. Cela signifie que le conducteur est le seul et unique thread de la piscine et que 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 contrôleur asynchrone, les passagers à l'arrière devraient attendre longtemps pendant que vous cherchiez votre argent, tandis que le chauffeur de bus ne travaillerait pas.

En conclusion, 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 à une question posée par le conducteur), les contrôleurs asynchrones pourraient aider le traitement des demandes, accélérant ainsi le processus. 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, l’async n’est donc pas un choix automatique.

48
K. Bob

Il y a deux concepts en jeu ici. Tout d'abord, nous pouvons faire en sorte que notre code soit exécuté en parallèle pour exécuter plus rapidement ou programmer le code sur un autre thread afin d'éviter que l'utilisateur n'attende. L'exemple que vous avez eu

public ViewResult Index() { 

    Task.Factory.StartNew(() => { 
        //Do an advanced looging here which takes a while
    });

    return View();
}

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 effectuer le même travail + gérer le threading.

Un autre exemple serait:

public ViewResult Index() { 

    Task.Factory.StartNew(() => { 
        //Make async web request to Twitter with WebClient.DownloadString()
    });

    Task.Factory.StartNew(() => { 
        //Make async web request to facebook with WebClient.DownloadString()
    });


    //wait for both to be ready and merge the results

    return View();
}

Étant donné que les demandes 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 savoir que nous utilisons plus de ressources ici que si nous utilisions des logiciels série car nous exécutons le code sur de nombreux threads pendant que nous les avons en attente.

Cela convient parfaitement dans un scénario client. Et il est assez courant d’envelopper du code long et synchrone dans une nouvelle tâche (exécutez-le sur un autre thread) et de garder l’utilisateur réactif ou de le mettre en parallèle pour le rendre plus rapide. Un fil est toujours utilisé pour toute la durée cependant. Sur un serveur très chargé, cela pourrait se retourner contre vous, car vous utilisez plus de ressources. C'est ce que les gens vous ont préven

Les contrôleurs asynchrones dans MVC ont cependant un autre objectif. Le but ici est d'éviter que les threads ne s'assoient sans rien faire (ce qui peut nuire à l'évolutivité). Ce n’est vraiment important 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 traiter les nouvelles demandes jusqu'à ce que la demande Web soit terminée, où il vous rappellera et obtiendra le même ou un nouveau thread et finira la demande.

J'espère que vous comprenez la différence entre asynchrone et parallèle. Pensez au code parallèle en tant que code dans lequel se trouve votre thread et attendez le résultat. Tandis que le code asynchrone est un code où vous serez averti lorsque le code est terminé et que vous pouvez y revenir, entre-temps, le thread peut effectuer un autre travail.

10
Mikael Eliasson

Les applications peut évoluent mieux si les opérations s'exécutent de manière asynchrone, mais niquement si des ressources sont disponibles pour gérer 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 en mode FIFO, mais cela ne fonctionnerait pas correctement lorsque des centaines de demandes sont en file d'attente et que chaque demande prend 100 ms à traiter.

Si vous avez un volume de trafic énorme, vous peut mieux vaut ne pas exécuter vos requêtes de manière asynchrone, car il peut ne pas y avoir de ressources supplémentaires pour répondre aux demandes . S'il n'y a pas de ressources inutilisées, vos demandes sont obligées de mettre en file d'attente, de prendre une durée exponentielle plus longue ou d'échouer complètement, auquel cas la surcharge asynchrone (mutex et opérations de commutation 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, sauf si vous essayez de gérer une ressource partagée entre toutes les demandes, vous ne constaterez 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 est subjectif jusqu'à ce que vous examiniez ce qui cause un goulot d'étranglement dans votre système. Parfois, il est évident qu'un modèle asynchrone peut aider (en empêchant le blocage d'une ressource 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é.

Edit:

Dans votre exemple, l'appel Task.Factory.StartNew 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 les coûts de création/destruction de nombreux threads). Une fois l'opération terminée, le thread est libéré dans le pool pour être réutilisé par une autre demande (le récupérateur de place n'intervient réellement que si vous avez créé des objets dans vos opérations. Dans ce 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 respecter la tâche asynchrone. La seule préoccupation peut être si votre pool de threads est saturé (c’est-à-dire qu’il n’ya pas de threads disponibles pour traiter la demande pour le moment et que les paramètres du pool ne permettent pas la création de threads supplémentaires), auquel cas la demande est bloquée en attente de démarrage de la tâche jusqu’à ce qu’un thread de pool soit disponible.

6
Paul Turner

Oui, ils utilisent un thread du pool de threads. Il existe actuellement 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 asynchrone ne l’améliore pas nécessairement et peut dans certains cas aggraver l’échelle de votre application. L'autre commentaire que vous avez posté sur "un volume de trafic énorme ..." 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 d'autres parties du système.

En bref, beaucoup de gens ont beaucoup d'opinions sur l'async, mais ils peuvent ne pas être corrects hors de leur contexte. Je dirais de vous concentrer sur vos problèmes et de faire des tests de performances de base pour voir quels contrôleurs asynchrones, etc. gèrent réellement votre application .

2
A.R.

La première chose n’est pas MVC mais le IIS qui gère le pool de threads. Ainsi, toute demande qui parvient à une application MVC ou ASP.NET est traitée à partir de threads gérés dans le pool de threads. Seulement en rendant l'application Asynch, il invoque cette action dans un autre thread et le libère immédiatement afin que d'autres demandes puissent être traitées.

J'ai expliqué la même chose avec une vidéo détaillée ( http://www.youtube.com/watch?v=wvg13n5V0V0/ "Contrôleurs MVC Asynch et manque de threads") qui montre comment le manque de threads se produit dans MVC et comment le minimiser en utilisant les contrôleurs MVC Asynch.J'ai aussi 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 MVC asynch et quelle est la pire pour les opérations Synch.

0
Shivprasad Koirala