As operações assíncronas no ASP.NET MVC usam um thread do ThreadPool no .NET 4

Após essa pergunta, fico confortável ao usar operações assíncronas no ASP.NET MVC. Então, eu escrevi dois posts sobre isso:

  • Minha opinião sobre a programação assíncrona baseada em tarefas em C # 5.0 e em aplicativos da Web ASP.NET MVC
  • Chamadas de database asynchronous com modelo de programação assíncrona baseado em tarefa (TAP) no ASP.NET MVC 4

Tenho muitos equívocos em mente sobre operações assíncronas no ASP.NET MVC.

Eu sempre ouço essa frase: O aplicativo pode ser dimensionado melhor se as operações forem executadas de forma assíncrona

E também ouvi esse tipo de frase: se você tem um volume enorme de tráfego, talvez seja melhor não executar suas consultas de forma assíncrona – consumir dois threads extras para atender a uma solicitação tira resources de outras solicitações recebidas.

Eu acho que essas duas frases são inconsistentes.

Eu não tenho muita informação sobre como threadpool funciona no asp.net, mas sei que o threadpool tem um tamanho limitado para threads. Então, a segunda sentença tem que estar relacionada a essa questão.

E gostaria de saber se operações assíncronas no ASP.NET MVC usam um thread do ThreadPool no .NET 4?

Por exemplo, quando implementamos um AsyncController, como o aplicativo é estruturado? Se eu obtiver um tráfego enorme, é uma boa ideia implementar o AsyncController?

Existe alguém lá fora que pode tirar essa cortina preta na frente dos meus olhos e me explicar o negócio sobre assynchronization na ASP.NET MVC 3 (NET 4)?

Editar:

Eu li este documento abaixo quase centenas de vezes e eu entendo o negócio principal, mas ainda tenho confusão porque há muitos comentários inconsistentes por aí.

Usando um controlador asynchronous no asp.net MVC

Editar:

Vamos supor que eu tenha a ação do controller como abaixo (não uma implementação do AsyncController ):

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

Como você vê aqui, eu faço uma operação e esqueço disso. Então, volto imediatamente sem esperar que seja completado.

Nesse caso, isso tem que usar um thread de threadpool? Em caso afirmativo, após a conclusão, o que acontece com esse segmento? O GC entra e limpa logo após a conclusão?

Editar:

Para a resposta do @ Darin, aqui está uma amostra do código asynchronous que fala com o database:

 public class FooController : AsyncController { //EF 4.2 DbContext instance MyContext _context = new MyContext(); public void IndexAsync() { AsyncManager.OutstandingOperations.Increment(3); Task<IEnumerable>.Factory.StartNew(() => { return _context.Foos; }).ContinueWith(t => { AsyncManager.Parameters["foos"] = t.Result; AsyncManager.OutstandingOperations.Decrement(); }); Task<IEnumerable>.Factory.StartNew(() => { return _context.Bars; }).ContinueWith(t => { AsyncManager.Parameters["bars"] = t.Result; AsyncManager.OutstandingOperations.Decrement(); }); Task<IEnumerable>.Factory.StartNew(() => { return _context.FooBars; }).ContinueWith(t => { AsyncManager.Parameters["foobars"] = t.Result; AsyncManager.OutstandingOperations.Decrement(); }); } public ViewResult IndexCompleted( IEnumerable foos, IEnumerable bars, IEnumerable foobars) { //Do the regular stuff and return } } 

Aqui está um excelente artigo que eu recomendo que você leia para entender melhor o processamento asynchronous no ASP.NET (que é basicamente o que os controladores asynchronouss representam).

Vamos primeiro considerar uma ação síncrona padrão:

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

Quando uma solicitação é feita para essa ação, um encadeamento é desenhado a partir do conjunto de encadeamentos e o corpo dessa ação é executado nesse encadeamento. Portanto, se o processamento dentro dessa ação for lento, você estará bloqueando esse segmento para todo o processamento, portanto, esse segmento não poderá ser reutilizado para processar outras solicitações. No final da execução da solicitação, o encadeamento é retornado ao conjunto de encadeamentos.

Agora vamos dar um exemplo do padrão asynchronous:

 public void IndexAsync() { // perform some processing } public ActionResult IndexCompleted(object result) { return View(); } 

Quando uma solicitação é enviada para a ação Index, um thread é desenhado a partir do pool de threads e o corpo do método IndexAsync é executado. Depois que o corpo desse método termina a execução, o thread é retornado ao pool de segmentos. Em seguida, usando o AsyncManager.OutstandingOperations padrão, depois de sinalizar a conclusão da operação assíncrona, outro thread é desenhado a partir do pool de threads e o corpo da ação IndexCompleted é executado nele e o resultado é renderizado para o cliente.

Então, o que podemos ver neste padrão é que um único pedido HTTP de cliente pode ser executado por dois threads diferentes.

Agora, a parte interessante acontece dentro do método IndexAsync . Se você tiver uma operação de bloqueio dentro dela, você estará desperdiçando totalmente o propósito dos controladores asynchronouss porque está bloqueando o thread de trabalho (lembre-se de que o corpo dessa ação é executado em um thread extraído do pool de threads).

Então, quando podemos tirar vantagem real dos controladores asynchronouss que você pode perguntar?

IMHO nós podemos ganhar mais quando temos operações intensivas de I / O (como database e chamadas de rede para serviços remotos). Se você tiver uma operação intensiva da CPU, as ações assíncronas não trarão muito benefício.

Então, por que podemos obter benefícios de operações intensivas de E / S? Porque poderíamos usar portas de conclusão de E / S. O IOCP é extremamente poderoso porque você não consome nenhum encadeamento ou recurso no servidor durante a execução de toda a operação.

Como eles funcionam?

Suponha que queremos baixar o conteúdo de uma página da Web remota usando o método WebClient.DownloadStringAsync . Você chama esse método que registrará um IOCP no sistema operacional e retornará imediatamente. Durante o processamento de toda a solicitação, nenhum encadeamento é consumido em seu servidor. Tudo acontece no servidor remoto. Isso pode levar muito tempo, mas você não se importa, pois não está colocando em risco seus threads de trabalho. Quando uma resposta é recebida, o IOCP é sinalizado, um thread é desenhado a partir do pool de threads e o callback é executado neste thread. Mas como você pode ver, durante todo o processo, não monopolizamos nenhum segmento.

O mesmo acontece com methods como FileStream.BeginRead, SqlCommand.BeginExecute, …

E quanto a paralelizar várias chamadas de database? Suponha que você tenha uma ação do controlador síncrono na qual você executou 4 chamadas de database de bloqueio em sequência. É fácil calcular que, se cada chamada do database levar 200ms, a ação do controlador levará cerca de 800ms para ser executada.

Se você não precisa executar essas chamadas sequencialmente, paralelizá-las melhoraria o desempenho?

Essa é a grande questão, que não é fácil de responder. Talvez sim, talvez não. Isso dependerá inteiramente de como você implementa essas chamadas de database. Se você usar controladores asynchronouss e portas de conclusão de E / S, conforme discutido anteriormente, você aumentará o desempenho dessa ação do controlador e também de outras ações, já que não monopolizará os encadeamentos do trabalhador.

Por outro lado, se você os implementar mal (com uma chamada de database de bloqueio executada em um encadeamento do conjunto de encadeamentos), basicamente reduzirá o tempo total de execução dessa ação para aproximadamente 200ms, mas consumiria 4 encadeamentos de trabalho pode ter degradado o desempenho de outras solicitações que podem se tornar famintas devido à falta de encadeamentos no conjunto para processá-las.

Portanto, é muito difícil e, se você não se sentir pronto para executar testes extensivos em seu aplicativo, não implemente controladores asynchronouss, pois provavelmente você fará mais danos do que benefícios. Implemente-os somente se tiver uma razão para fazer isso: por exemplo, você identificou que as ações do controlador síncrono padrão são um gargalo para o seu aplicativo (depois de realizar extensos testes de carga e medições, é claro).

Agora vamos considerar seu exemplo:

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

Quando uma solicitação é recebida para a ação Index, um thread é desenhado a partir do pool de threads para executar seu corpo, mas seu corpo apenas agenda uma nova tarefa usando o TPL . Portanto, a execução da ação é finalizada e o encadeamento é retornado ao conjunto de encadeamentos. Exceto que, o TPL usa encadeamentos do conjunto de encadeamentos para executar seu processamento. Portanto, mesmo que o thread original tenha sido retornado para o pool de threads, você desenhou outro thread desse pool para executar o corpo da tarefa. Então você prejudicou 2 tópicos de sua preciosa piscina.

Agora vamos considerar o seguinte:

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

Neste caso, estamos manualmente gerando um thread. Nesse caso, a execução do corpo da ação Index pode demorar um pouco mais (porque gerar um novo thread é mais caro do que desenhar um de um pool existente). Mas a execução da operação de log avançada será feita em um encadeamento que não faz parte do pool. Portanto, não estamos comprometendo os encadeamentos do conjunto, que permanecem livres para atender a outros pedidos.

Sim – todos os threads vêm do pool de threads. Seu aplicativo MVC já é multi-threaded, quando uma solicitação chega em um novo segmento será retirado do pool e usado para atender a solicitação. Esse segmento será ‘bloqueado’ (de outras solicitações) até que a solicitação seja totalmente atendida e concluída. Se não houver nenhum thread disponível no pool, o pedido terá que esperar até que um esteja disponível.

Se você tiver controladores asynchronouss, eles ainda obterão um encadeamento do pool, mas enquanto estiverem atendendo a solicitação, poderão desistir do encadeamento, enquanto esperam que algo aconteça (e esse encadeamento pode ser fornecido para outro pedido) e quando o pedido original precisar de um encadeamento novamente, recebe um da piscina.

A diferença é que, se você tiver muitos pedidos de longa execução (em que o encadeamento está aguardando uma resposta de algo), poderá ficar sem encadeamentos do conjunto para atender a solicitações básicas. Se você tiver controladores asynchronouss, não haverá mais encadeamentos, mas esses encadeamentos que estão aguardando serão retornados ao pool e poderão atender a outras solicitações.

Um exemplo de vida quase real … Pense nisso como pegar um ônibus, há cinco pessoas esperando para entrar, o primeiro é ligado, paga e se senta (o motorista atende o pedido deles), você entra (o motorista está fazendo manutenção seu pedido), mas você não pode encontrar o seu dinheiro; enquanto você se atrapalha nos bolsos, o motorista desiste de você e pega as próximas duas pessoas (atendendo seus pedidos), quando você encontra seu dinheiro, o motorista começa a lidar com você novamente (completando seu pedido) – a quinta pessoa tem que esperar até você está feito, mas a terceira e quarta pessoas foram servidas enquanto você estava no meio do caminho sendo servido. Isso significa que o motorista é o primeiro e único segmento da piscina e os passageiros são os pedidos. Era complicado demais escrever como funcionaria se houvesse dois motoristas, mas você pode imaginar …

Sem um controlador asynchronous, os passageiros atrás de você teriam que esperar muito enquanto você procurava o seu dinheiro, enquanto o motorista do ônibus não estaria fazendo nenhum trabalho.

Portanto, a conclusão é que, se muitas pessoas não sabem onde está seu dinheiro (ou seja, exigem muito tempo para responder a algo que o motorista tenha solicitado), os controladores asynchronouss poderiam ajudar na transferência de solicitações, acelerando o processo de alguns. Sem um controlador de aysnc, todos esperam até que a pessoa na frente tenha sido completamente tratada. Mas não se esqueça que no MVC você tem muitos drivers de barramento em um único barramento, assim o asynchronous não é uma opção automática.

Existem dois conceitos em jogo aqui. Primeiro de tudo, podemos fazer nosso código rodar em paralelo para executar mais rápido ou programar o código em outro thread para evitar que o usuário espere. O exemplo que você teve

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

pertence à segunda categoria. O usuário obterá uma resposta mais rápida, mas a carga de trabalho total no servidor será maior, pois ele terá que executar o mesmo trabalho + manipular o encadeamento.

Outro exemplo disso seria:

 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(); } 

Como os pedidos são executados em paralelo, o usuário não terá que esperar, desde que eles sejam feitos em série. Mas você deve perceber que nós usamos mais resources aqui do que se nós rodássemos em serial porque nós rodamos o código em muitos threads enquanto nós temos um thread esperando também.

Isso está perfeitamente bem em um cenário de cliente. E é muito comum haver um código síncrono de execução longa em uma nova tarefa (executá-lo em outro thread) também manter o ui responsivo ou paralelo para torná-lo mais rápido. Um thread ainda é usado por toda a duração. Em um servidor com carga alta, isso pode sair pela culatra porque você realmente usa mais resources. Isto é o que as pessoas te avisaram sobre

Os controladores asynchronouss no MVC têm outro objective. O ponto aqui é evitar que as sessões de threads não façam nada (o que pode prejudicar a escalabilidade). Realmente só importa se a API que você está chamando tiver methods asynchronouss. Como WebClient.DowloadStringAsync ().

O ponto é que você pode deixar seu segmento ser retornado para manipular novas solicitações até que a solicitação da web seja concluída, onde ele irá chamá-lo de retorno de chamada que obtém o mesmo ou um novo segmento e concluir a solicitação.

Espero que você entenda a diferença entre asynchronous e paralelo. Pense no código paralelo como o código em que o seu segmento fica por perto e aguarde o resultado. Enquanto código asynchronous é o código onde você será notificado quando o código é feito e você pode voltar a trabalhar nele, enquanto isso, o thread pode fazer outro trabalho.

Os aplicativos podem ser dimensionados melhor se as operações forem executadas de forma assíncrona, mas somente se houver resources disponíveis para atender às operações adicionais .

As operações assíncronas garantem que você nunca esteja bloqueando uma ação porque uma já existente está em andamento. O ASP.NET possui um modelo asynchronous que permite que várias solicitações sejam executadas lado a lado. Seria possível enfileirar as solicitações e processá-las como FIFO, mas isso não escalaria bem quando você tiver centenas de solicitações em fila e cada solicitação levar 100ms para ser processada.

Se você tiver um volume enorme de tráfego, talvez seja melhor não executar suas consultas de forma assíncrona, pois pode não haver resources adicionais para atender às solicitações . Se não houver resources sobressalentes, suas solicitações serão forçadas a entrar na fila, sofrerão uma falha exponencialmente mais longa ou definitiva e, nesse caso, a sobrecarga assíncrona (mutexes e operações de alternância de contexto) não fornecerá nada.

No que diz respeito ao asp.net, você não tem escolha – ele usa um modelo asynchronous, porque é isso que faz sentido para o modelo servidor-cliente. Se você estivesse escrevendo seu próprio código internamente que usa um padrão asynchronous para tentar dimensionar melhor, a menos que você esteja tentando gerenciar um recurso que seja compartilhado entre todas as solicitações, não verá melhorias porque elas já estão envolvidas em um processo asynchronous que não bloqueia nada.

Em última análise, é tudo subjetivo até você realmente olhar para o que está causando um gargalo no seu sistema. Às vezes, é óbvio que um padrão asynchronous ajudará (impedindo o bloqueio de um recurso enfileirado). Em última análise, apenas medir e analisar um sistema pode indicar onde você pode obter eficiências.

Editar:

No seu exemplo, a chamada Task.Factory.StartNew colocará em fila uma operação no conjunto de encadeamentos .NET. A natureza dos encadeamentos do Conjunto de Encadeamentos deve ser reutilizada (para evitar o custo de criar / destruir muitos encadeamentos). Depois que a operação for concluída, o thread é liberado de volta ao pool para ser reutilizado por outro pedido (o Garbage Collector não se envolve de fato, a menos que você tenha criado alguns objects em suas operações e, nesse caso, eles serão coletados conforme o normal escopo).

No que diz respeito ao ASP.NET, não há operação especial aqui. A solicitação do ASP.NET é concluída sem respeito à tarefa assíncrona. A única preocupação pode ser se o pool de threads estiver saturado (ou seja, não houver encadeamentos disponíveis para atender a solicitação agora e as configurações do pool não permitirem que mais encadeamentos sejam criados). Nesse caso, a solicitação será bloqueada para iniciar o tarefa até que um encadeamento de pool fique disponível.

Sim, eles usam um thread do pool de threads. Na verdade, há um excelente guia do MSDN que abordará todas as suas perguntas e muito mais. Eu descobri que é bastante útil no passado. Confira!

http://msdn.microsoft.com/pt-br/library/ee728598.aspx

Enquanto isso, os comentários + sugestões que você ouve sobre o código asynchronous devem ser tomados com um pouco de sal. Para começar, apenas fazer algo asynchronous não necessariamente faz com que ele seja escalonado melhor e, em alguns casos, pode piorar a escala de sua aplicação. O outro comentário que você postou sobre “um enorme volume de tráfego …” também está correto apenas em certos contextos. Isso realmente depende do que suas operações estão fazendo e de como elas interagem com outras partes do sistema.

Em suma, muitas pessoas têm muitas opiniões sobre async, mas podem não estar corretas fora de contexto. Eu diria que concentre-se em seus problemas exatos e faça testes básicos de desempenho para ver quais controladores asynchronouss, etc. realmente lidam com seu aplicativo.

A primeira coisa que não é MVC, mas o IIS que mantém o pool de threads. Portanto, qualquer solicitação proveniente do aplicativo MVC ou ASP.NET é fornecida a partir de encadeamentos que são mantidos no conjunto de encadeamentos. Somente com a criação do aplicativo Asynch ele invoca essa ação em um thread diferente e libera o thread imediatamente para que outros pedidos possam ser tomados.

Eu expliquei o mesmo com um vídeo detalhado ( http://www.youtube.com/watch?v=wvg13n5V0V0/ “controladores MVC Asynch e thread starvation”) que mostra como a privação de thread acontece no MVC e como é minimizada usando MVC Controladores Asynch.Também medi as filas de pedidos usando o perfmon para que você possa ver como as filas de pedidos são diminuídas para o MVS e como é pior para as operações de Sincronização.