157 votes

Les opérations asynchrones dans ASP.NET MVC utilisent-elles un thread du ThreadPool sur .NET 4 ?

Après cette question, je suis plus à l'aise lorsque j'utilise des 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 concernant les opérations asynchrones sur ASP.NET MVC.

J'entends toujours cette phrase : L'application peut mieux évoluer si les opérations sont exécutées de manière asynchrone.

Et j'ai aussi beaucoup entendu ce genre de phrases : si vous avez un énorme volume de trafic, il est préférable de ne pas exécuter vos requêtes de manière asynchrone - la consommation de deux threads supplémentaires pour répondre à une requête prive de ressources les autres requêtes entrantes.

Je pense que ces deux phrases sont incohérentes.

Je n'ai pas beaucoup d'informations sur le fonctionnement du pool de threads en ASP.NET mais je sais que le pool de threads a une taille limitée pour les threads. Donc, la deuxième phrase doit être liée à ce problème.

Et je voudrais savoir si les opérations asynchrones dans ASP.NET MVC utilisent un thread du ThreadPool sur .NET 4 ?

Par exemple, lorsque nous mettons en œuvre un AsyncController, comment l'application se structure-t-elle ? Si j'ai un trafic énorme, est-ce une bonne idée de mettre en œuvre AsyncController ?

Y a-t-il quelqu'un qui puisse enlever ce rideau noir devant mes yeux et m'expliquer ce qu'est l'asynchronie sur ASP.NET MVC 3 (NET 4) ?

Editar:

J'ai lu le document ci-dessous des centaines de fois et j'en ai compris l'essentiel, mais je reste confus car il y a trop de commentaires incohérents.

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

Editar:

Supposons que j'ai une action de contrôleur comme ci-dessous (qui n'est pas une mise en œuvre de l'option 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 je l'oublie. Puis, je reviens immédiatement sans attendre qu'elle soit terminée.

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

Editar:

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

    }
}

176voto

Darin Dimitrov Points 528142

Voici un excellent article Je vous recommande une lecture 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 :

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

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. Ainsi, si le traitement à l'intérieur de cette action est lent, vous bloquez ce thread pour l'ensemble du traitement, de sorte que ce thread ne peut pas être réutilisé pour traiter d'autres demandes. À la fin de l'exécution de la demande, le thread est renvoyé dans le pool de threads.

Prenons maintenant un exemple du modèle 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 demande est envoyé à l'action Index. IndexAsync est exécutée. 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 méthode standard AsyncManager.OutstandingOperations Dès que vous signalez l'achèvement de l'opération asynchrone, un autre thread est tiré du pool de threads et le corps de l'opération asynchrone est lancé. IndexCompleted est exécutée sur elle et le résultat est rendu au client.

Ce que nous pouvons voir dans ce modèle, c'est qu'une seule requête HTTP du client peut être exécutée par deux threads différents.

Maintenant, la partie intéressante se passe à l'intérieur du IndexAsync méthode. Si vous avez une opération bloquante à l'intérieur, vous gâchez totalement l'objectif des contrôleurs asynchrones car vous bloquez le fil de travail (rappelez-vous que le corps de cette action est exécuté sur un fil tiré du pool de fils).

Vous pourriez donc vous demander quand nous pouvons réellement tirer parti des contrôleurs asynchrones.

À mon avis, nous avons le plus à gagner lorsque nous avons des opérations à forte intensité d'E/S (comme les appels de base de données et de 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 profit des opérations à forte intensité d'E/S ? Parce que nous pouvons utiliser Ports d'achèvement d'E/S . Les IOCP sont extrêmement puissants car ils ne consomment pas de threads ou de ressources sur le serveur pendant l'exécution de toute l'opération.

Comment fonctionnent-ils ?

Supposons que nous voulions télécharger le contenu d'une page web distante à l'aide de la fonction WebClient.DownloadStringAsync méthode. Vous appelez cette méthode qui va enregistrer un IOCP dans le système d'exploitation et revenir immédiatement. Pendant le traitement de l'ensemble de la demande, aucun thread n'est consommé sur votre serveur. Tout se passe sur le serveur distant. Cela peut prendre beaucoup de temps mais vous ne vous en souciez pas car vous ne mettez pas en péril vos threads de travail. Une fois la réponse reçue, l'IOCP est signalé, un thread est tiré du pool de threads et le callback 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 pour les 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 effectuez 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'exécution de votre action de contrôleur prendra environ 800 ms.

Si vous n'avez pas besoin d'exécuter ces appels séquentiellement, le fait de les paralléliser améliorerait-il 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 de complétion d'E/S comme nous l'avons vu précédemment, vous améliorerez les performances de cette action de contrôle et d'autres actions également, car vous ne monopoliserez pas les threads de travail.

D'un autre côté, si vous les implémentez mal (avec un appel bloquant à la base de données 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 aurez consommé 4 threads de travail et vous risquez de dégrader les performances d'autres requêtes qui pourraient devenir affamées parce qu'il manque des threads 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 mettez en œuvre que si vous avez une raison de le faire : par exemple, vous avez identifié que les actions du contrôleur synchrone standard sont un goulot d'étranglement pour votre application (après avoir effectué des tests de charge et des mesures approfondies bien sûr).

Prenons 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 ce dernier ne fait que planifier une nouvelle tâche à l'aide de la fonction TPL . L'exécution de l'action se termine donc 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 est retourné dans le pool de threads, vous avez tiré 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 :

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 de temps (car la création d'un nouveau thread est plus coûteuse que l'extraction d'un thread d'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. Ainsi, nous ne mettons pas en danger les threads du pool qui restent libres pour servir d'autres demandes.

47voto

K. Bob Points 938

Oui - tous les fils proviennent du pool de fils. Votre application MVC est déjà multithreadée. Lorsqu'une demande arrive, un nouveau thread est pris dans le pool et utilisé pour répondre à la demande. Ce thread sera "verrouillé" (par rapport aux 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 qu'un thread soit disponible.

Si vous avez des contrôleurs asynchrones, ils obtiennent toujours un thread du pool, mais pendant qu'ils traitent la demande, ils peuvent abandonner le thread, en attendant que quelque chose se passe (et ce thread peut être donné à une autre demande) et quand la demande originale a besoin d'un thread à nouveau, elle en obtient un du pool.

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 aux requêtes les plus simples. 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 répondre à d'autres demandes.

A presque Un exemple concret... Imaginez que vous montez dans un bus, il y a cinq personnes qui attendent de monter, la première monte, paie et s'assoit (le chauffeur a répondu à sa demande), vous montez (le chauffeur répond à votre demande) mais vous ne trouvez pas votre argent ; pendant que vous fouillez dans vos poches, le conducteur vous abandonne et fait monter les deux personnes suivantes (il répond à leurs demandes), lorsque vous trouvez votre argent, le conducteur recommence à s'occuper de vous (il répond à votre demande) - la cinquième personne doit attendre que vous ayez terminé, mais les troisième et quatrième personnes ont été servies pendant que vous étiez en train de vous faire servir. Cela signifie que le conducteur est le seul et unique fil de la piscine et que les passagers sont les demandes. Il était trop compliqué d'écrire comment cela fonctionnerait s'il y avait deux conducteurs, mais vous pouvez imaginer...

Sans contrôleur asynchrone, les passagers derrière vous devraient attendre longtemps pendant que vous cherchez votre argent, tandis que le conducteur du bus ne ferait aucun travail.

La conclusion est donc que si beaucoup de gens ne savent pas où est leur argent (c'est-à-dire qu'ils ont besoin d'un long temps pour répondre à une demande du pilote), les contrôleurs asynchrones pourraient bien aider le débit des demandes, en accélérant le processus pour certains. Sans contrôleur aysnc, tout le monde attend que la personne qui précède ait été complètement traitée. MAIS n'oubliez pas qu'en MVC vous avez beaucoup de pilotes de bus sur un seul bus, donc l'asynchronisme n'est pas un choix automatique.

10voto

Mikael Eliasson Points 2962

Deux concepts sont en jeu ici. Tout d'abord, nous pouvons faire en sorte que notre code s'exécute en parallèle pour une exécution plus rapide ou planifier le code sur un autre thread pour éviter de faire attendre l'utilisateur. 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 obtient une réponse plus rapide, mais la charge de travail totale du serveur est plus élevée, car il doit effectuer le même travail et 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();
}

Comme les demandes sont exécutées en parallèle, l'utilisateur n'aura pas à attendre aussi longtemps que si elles étaient exécutées en série. Mais vous devez réaliser que nous utilisons plus de ressources ici que si nous fonctionnons en série parce que nous exécutons le code à plusieurs threads pendant que nous avons un thread en attente aussi.

C'est tout à fait normal dans le cas d'un client. Et il est assez courant dans ce cas d'envelopper du code synchrone à long terme dans une nouvelle tâche (l'exécuter sur un autre thread) pour que l'interface utilisateur reste réactive ou pour le paraléliser afin de le rendre plus rapide. Cependant, un thread est toujours utilisé pendant toute la durée de l'opération. Sur un serveur à forte charge, cela peut se retourner contre vous car vous utilisez plus de ressources. C'est ce dont les gens vous ont averti

Les contrôleurs asynchrones dans MVC ont cependant un autre objectif. Il s'agit ici d'éviter d'avoir des threads qui restent assis à ne rien faire (ce 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().

L'intérêt est que vous pouvez laisser votre thread être retourné pour gérer les nouvelles demandes jusqu'à ce que la demande web soit terminée, où il appellera votre callback qui obtient le même ou un nouveau thread et termine la demande.

J'espère que vous comprenez la différence entre asynchrone et parallèle. Pensez au code parallèle comme un code où votre thread reste assis et attend le résultat. Alors que le code asynchrone est un code dans lequel vous serez notifié lorsque le code sera terminé et que vous pourrez vous remettre au travail, pendant ce temps, le thread peut faire autre chose.

6voto

Tragedian Points 12308

Applications puede s'échelonnent mieux si les opérations sont exécutées de manière asynchrone, mais uniquement si des ressources sont disponibles pour assurer les opérations supplémentaires. .

Les opérations asynchrones garantissent que vous ne bloquez jamais une action parce qu'une autre est en cours. ASP.NET dispose d'un modèle asynchrone qui permet à plusieurs demandes de s'exécuter côte à côte. Il serait possible de mettre les requêtes en file d'attente et de les traiter en FIFO, mais cela ne serait pas très efficace si vous aviez des centaines de requêtes en file d'attente et que chaque requête prenait 100 ms à traiter.

Si vous avez un énorme volume de trafic, vous mai Il est préférable de 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 répondre aux demandes. . S'il n'y a pas de ressources disponibles, vos demandes sont obligées de faire la queue, de prendre un temps exponentiel ou d'échouer purement et simplement, auquel cas les frais généraux asynchrones (mutex et opérations de changement de contexte) ne vous apportent rien.

En ce qui concerne ASP.NET, vous n'avez pas le choix - il utilise un modèle asynchrone, car c'est ce qui est logique pour le modèle serveur-client. Si vous deviez écrire votre propre code interne qui utilise un modèle asynchrone pour tenter d'améliorer l'évolutivité, à moins que vous n'essayiez de gérer une ressource partagée entre toutes les requêtes, vous ne verriez pas vraiment d'améliorations car elles sont déjà enveloppées dans un processus asynchrone qui ne bloque rien d'autre.

En fin de compte, tout est subjectif jusqu'à ce que vous examiniez réellement 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 définitive, seules la mesure et l'analyse d'un système peuvent indiquer où vous pouvez gagner en efficacité.

Editar:

Dans votre exemple, le Task.Factory.StartNew mettra en file d'attente une opération sur le pool de threads de .NET. La nature des threads du pool est d'être réutilisés (pour éviter le coût de la 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 Garbage Collector n'intervient pas vraiment, sauf si vous avez créé des objets dans vos opérations, auquel cas ils sont collectés selon la procédure normale).

En ce qui concerne ASP.NET, il n'y a pas d'opération spéciale ici. La requête ASP.NET se termine sans tenir compte de la tâche asynchrone. Le seul problème pourrait être que votre pool de threads est saturé (c'est-à-dire qu'il n'y a pas de threads disponibles pour traiter la demande en ce moment et que les paramètres du pool ne permettent pas la création de nouveaux threads), auquel cas la demande est bloquée. en attente du démarrage de la tâche jusqu'à ce qu'un fil de pool soit disponible.

3voto

RickAnd - MSFT Points 3741

Quand j'ai écrit Utilisation d'un contrôleur asynchrone dans ASP.NET MVC J'ai également écrit un blog Mes appels à la base de données doivent-ils être asynchrones ? décourageant l'utilisation de contrôleurs asynchrones (sauf si vous avez vraiment fait vos devoirs). J'ai suivi ce blog avec S es appels à ma base de données doivent-ils être asynchrones ? Partie II qui explique quand vous devez envisager l'asynchronisme. Comme le souligne mon blog, la recommandation canonique d'utiliser l'asynchrone pour les DB est presque toujours fausse. ASP.NET MVC 4 proposera probablement le support des tâches et des tâches pour les classes AsyncController - ce qui devrait rendre la programmation asynchrone plus simple (mais toujours follement complexe par rapport à la programmation synchrone).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X