Após essa pergunta, me sinto confortável ao usar operações assíncronas no ASP.NET MVC. Então, eu escrevi dois posts sobre isso:
Tenho muitos mal-entendidos em mente sobre operações assíncronas no ASP.NET MVC.
Eu sempre ouço esta frase: o aplicativo pode ser melhor dimensionado se as operações forem executadas de forma assíncrona
E também ouvi esse tipo de frase: se você tem um grande volume de tráfego, é melhor não executar suas consultas de forma assíncrona - consumir 2 threads extras para atender a uma solicitação retira recursos de outras solicitações recebidas.
Eu acho que essas duas frases são inconsistentes.
Não tenho muitas informações sobre como o threadpool funciona no ASP.NET, mas sei que o threadpool tem um tamanho limitado para threads. Portanto, a segunda frase deve estar relacionada a esse problema.
E gostaria de saber se as 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 estrutura? Se eu receber um tráfego enorme, é uma boa ideia implementar o AsyncController?
Existe alguém por aí que possa tirar essa cortina preta diante dos meus olhos e me explicar o acordo sobre assincronia no ASP.NET MVC 3 (NET 4)?
Editar:
Eu li o documento abaixo quase centenas de vezes e compreendo o negócio principal, mas ainda tenho confusão porque há muitos comentários inconsistentes por aí.
Usando um controlador assíncrono no asp.net MVC
Editar:
Vamos supor que eu tenha uma ação do controlador como abaixo (não uma implementação do AsyncController
que):
public ViewResult Index() {
Task.Factory.StartNew(() => {
//Do an advanced looging here which takes a while
});
return View();
}
Como você vê aqui, eu aciono uma operação e a esqueço. Então, volto imediatamente sem esperar que seja concluída.
Nesse caso, isso precisa usar um thread do threadpool? Se sim, após a conclusão, o que acontece com esse segmento? Ele GC
entra e limpa logo após a conclusão?
Editar:
Para a resposta do @ Darin, aqui está um exemplo de código assíncrono que fala com o banco de dados:
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
}
}
fonte
Respostas:
Aqui está um excelente artigo que eu recomendo que você leia para entender melhor o processamento assíncrono no ASP.NET (que é basicamente o que os controladores assíncronos representam).
Vamos primeiro considerar uma ação síncrona padrão:
Quando uma solicitação é feita para essa ação, um encadeamento é retirado 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 encadeamento durante todo o processamento, portanto, esse encadeamento 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 assíncrono:
Quando uma solicitação é enviada para a ação Índice, um thread é desenhado do pool de threads e o corpo do
IndexAsync
método é executado. Depois que o corpo desse método termina a execução, o thread é retornado ao pool de threads. Em seguida, usando o padrãoAsyncManager.OutstandingOperations
, depois de sinalizar a conclusão da operação assíncrona, outro encadeamento é retirado do conjunto de encadeamentos e o corpo daIndexCompleted
ação é executado nele e o resultado é renderizado ao cliente.Portanto, o que podemos ver nesse padrão é que uma única solicitação HTTP do cliente pode ser executada por dois threads diferentes.
Agora a parte interessante acontece dentro do
IndexAsync
método. Se você tiver uma operação de bloqueio, estará desperdiçando totalmente todo o propósito dos controladores assíncronos porque está bloqueando o encadeamento de trabalho (lembre-se de que o corpo desta ação é executado em um encadeamento retirado do conjunto de encadeamentos).Então, quando podemos tirar proveito real dos controladores assíncronos, você pode perguntar?
IMHO, podemos ganhar mais quando temos operações intensivas de E / S (como chamadas de banco de dados e rede para serviços remotos). Se você tiver uma operação intensiva de CPU, ações assíncronas não trarão muitos benefícios.
Então, por que podemos obter benefícios com operações intensivas de E / S? Porque nós poderíamos usar portas I / O de conclusão . 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 desejemos 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 thread é consumido no seu servidor. Tudo acontece no servidor remoto. Isso pode levar muito tempo, mas você não se importa, pois não está comprometendo seus threads de trabalho. Depois que uma resposta é recebida, o IOCP é sinalizado, um encadeamento é retirado do conjunto de encadeamentos e o retorno de chamada é executado nesse encadeamento. Mas como você pode ver, durante todo o processo, não monopolizamos nenhum thread.
O mesmo vale para métodos como FileStream.BeginRead, SqlCommand.BeginExecute, ...
Que tal paralelizar várias chamadas de banco de dados? Suponha que você tenha realizado uma ação de controlador síncrona na qual executou 4 chamadas de banco de dados de bloqueio em sequência. É fácil calcular que, se cada chamada do banco de dados demorar 200ms, a ação do seu controlador levará aproximadamente 800ms para ser executada.
Se você não precisar executar essas chamadas seqüencialmente, paralelizá-las melhoraria o desempenho?
Essa é a grande questão, que não é fácil de responder. Talvez sim, talvez não. Depende inteiramente de como você implementa essas chamadas ao banco de dados. Se você usar controladores assíncronos e portas de conclusão de E / S, conforme discutido anteriormente, você aumentará o desempenho dessa ação do controlador e de outras ações, pois não monopolizará os encadeamentos de trabalho.
Por outro lado, se você implementá-las de maneira inadequada (com uma chamada de banco de dados de bloqueio realizada em um encadeamento do pool de encadeamentos), você basicamente reduzirá o tempo total de execução dessa ação para aproximadamente 200ms, mas consumiria 4 encadeamentos de trabalho para que você pode ter prejudicado o desempenho de outras solicitações que podem passar fome devido à falta de threads no pool 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 assíncronos, pois é possível que você cause mais danos do que benefícios. Implemente-os apenas se você tiver um motivo para fazê-lo: por exemplo, você identificou que as ações padrão do controlador síncrono são um gargalo para o seu aplicativo (depois de executar testes de carga e medições abrangentes).
Agora vamos considerar o seu exemplo:
Quando uma solicitação é recebida para a ação Índice, um encadeamento é retirado do conjunto de encadeamentos para executar seu corpo, mas seu corpo apenas agenda uma nova tarefa usando o TPL . Portanto, a execução da ação termina e o encadeamento é retornado ao conjunto de encadeamentos. Exceto que, o TPL usa threads do pool de threads para executar seu processamento. Portanto, mesmo que o encadeamento original tenha sido retornado ao conjunto de encadeamentos, você desenhou outro encadeamento desse conjunto para executar o corpo da tarefa. Então você colocou em risco 2 threads do seu pool precioso.
Agora vamos considerar o seguinte:
Nesse caso, estamos gerando um thread manualmente. 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 extrair um de um pool existente). Mas a execução da operação avançada de log será feita em um encadeamento que não faz parte do pool. Portanto, não estamos comprometendo os threads do pool que permanecem livres para atender a outras solicitações.
fonte
System.Threading.Task
) rodando dentro doIndexAsync
método. Dentro dessas operações, estamos fazendo chamadas de banco de dados para um servidor. Então, todas elas são operações intensivas de E / S, certo? Nesse caso, criamos 4 threads separados (ou obtemos 4 threads separados do pool de threads)? Supondo que eu tenha uma máquina com vários núcleos, eles também serão executados em paralelo, certo?SqlCommand.ExecuteReader
você está desperdiçando tudo, pois esta é uma chamada de bloqueio. Você está bloqueando o encadeamento no qual essa chamada é executada e, se esse encadeamento for um encadeamento do pool, é muito ruim. Você se beneficiará somente se você usar I / Ports O Conclusão:SqlCommand.BeginExecuteReader
. Se você não usar o IOCP, não importa o que faça, não use controladores assíncronos, pois você causará mais danos do que benefícios no desempenho geral do seu aplicativo._context.Foo
não está executando nada. Você está apenas construindo uma árvore de expressão. Seja extremamente cuidadoso com isso. A execução da consulta é adiada apenas quando você começa a enumerar sobre o conjunto de resultados. E se isso acontecer na visão, isso pode ser catastrófico para o desempenho. Para executar ansiosamente uma consulta EF, adicione.ToList()
no final.Sim - todos os threads vêm do pool de threads. Seu aplicativo MVC já é multiencadeado, quando uma solicitação chega em um novo encadeamento será retirado do pool e usado para atender à solicitação. Esse encadeamento será 'bloqueado' (de outras solicitações) até que a solicitação seja totalmente atendida e concluída. Se não houver um encadeamento disponível no pool, a solicitação terá que esperar até que um esteja disponível.
Se você tiver controladores assíncronos, eles ainda receberão um encadeamento do pool, mas enquanto atendem à solicitação, eles podem desistir do encadeamento, enquanto aguardam que algo aconteça (e esse encadeamento pode ser fornecido para outra solicitação) e quando a solicitação original precisar de um encadeamento novamente ele recebe um da piscina.
A diferença é que, se você tiver muitas solicitações de execução longa (em que o encadeamento aguarda uma resposta de alguma coisa), poderá ficar sem encadeamentos do pool para atender a solicitações básicas. Se você possui controladores assíncronos, não possui mais threads, mas os threads que estão aguardando são retornados ao pool e podem atender a outras solicitações.
Um exemplo da vida quase real ... Pense nisso como entrar em um ônibus, há cinco pessoas esperando para entrar, a primeira entra, paga e senta (o motorista atendeu a sua solicitação), você continua (o motorista está atendendo seu pedido), mas você não consegue encontrar seu dinheiro; enquanto você mexe nos bolsos, o motorista desiste de você e coloca as próximas duas pessoas (atendendo a seus pedidos); quando você encontra seu dinheiro, o motorista começa a lidar com você novamente (concluindo seu pedido) - a quinta pessoa tem que esperar até você terminou, mas a terceira e quarta pessoas foram servidas enquanto você estava no meio do caminho. Isso significa que o motorista é o único fio da piscina e os passageiros são os pedidos. Era muito complicado escrever como funcionaria se houvesse dois drivers, mas você pode imaginar ...
Sem um controlador assíncrono, os passageiros atrás de você teriam que esperar muito tempo enquanto procurava seu dinheiro, enquanto o motorista do ônibus não faria nenhum trabalho.
Portanto, a conclusão é que, se muitas pessoas não souberem onde está seu dinheiro (por exemplo, exigir muito tempo para responder a algo que o motorista pediu), os controladores assíncronos podem muito bem ajudar no processamento de solicitações, acelerando o processo de alguns. Sem um controlador aysnc, todo mundo espera até que a pessoa na frente seja completamente tratada. MAS não esqueça que no MVC você tem muitos drivers de barramento em um único barramento, portanto, a assíncrona não é uma opção automática.
fonte
Existem dois conceitos em jogo aqui. Primeiro, podemos fazer com que nosso código seja executado em paralelo para executar mais rapidamente ou agendar código em outro encadeamento para evitar que o usuário espere. O exemplo que você teve
pertence à segunda categoria. O usuário receberá uma resposta mais rápida, mas a carga de trabalho total no servidor é maior porque precisa fazer o mesmo trabalho + lidar com o encadeamento.
Outro exemplo disso seria:
Como as solicitações são executadas em paralelo, o usuário não precisará esperar o tempo que for feito em série. Mas você deve perceber que usamos mais recursos aqui do que se rodássemos em série, porque rodamos o código em muitos threads enquanto ainda estamos esperando.
Isso é perfeitamente adequado em um cenário de cliente. E é bastante comum envolver o código síncrono de execução longa em uma nova tarefa (executá-lo em outro encadeamento) e também manter a interface do usuário responsiva ou paralela para torná-la mais rápida. 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 recursos. É sobre isso que as pessoas o alertaram
Os controladores assíncronos no MVC têm outro objetivo. O objetivo aqui é evitar que as seções de roscas fiquem sem fazer nada (o que pode prejudicar a escalabilidade). Realmente importa apenas se as APIs que você está chamando têm métodos assíncronos. Como WebClient.DowloadStringAsync ().
O ponto é que você pode permitir que seu encadeamento seja retornado para lidar com novas solicitações até que a solicitação da web seja concluída, onde será chamado o retorno de chamada que obtém o mesmo ou um novo encadeamento e finaliza a solicitação.
Espero que você entenda a diferença entre assíncrono e paralelo. Pense no código paralelo como o código em que seu encadeamento fica e aguarde o resultado. Embora o código assíncrono seja o código em que você será notificado quando o código for concluído e poderá voltar a trabalhar nele, enquanto isso, o encadeamento pode executar outro trabalho.
fonte
Os aplicativos podem escalar melhor se as operações forem executadas de forma assíncrona, mas apenas se houver recursos disponíveis para atender as operações adicionais .
As operações assíncronas garantem que você nunca bloqueie uma ação porque uma existente está em andamento. O ASP.NET possui um modelo assíncrono que permite que várias solicitações sejam executadas lado a lado. Seria possível enfileirar as solicitações e processá-las FIFO, mas isso não seria escalável quando você tem centenas de solicitações na fila e cada solicitação leva 100ms para processar.
Se você tem um grande volume de tráfego, você pode ser melhor fora de não realizar suas consultas de forma assíncrona, como pode não haver recursos adicionais para atender as solicitações . Se não houver recursos sobressalentes, suas solicitações serão forçadas a ficar na fila, levar uma falha exponencialmente mais longa ou definitiva; nesse caso, a sobrecarga assíncrona (operações de mutex e mutação de contexto) não está fornecendo nada.
No que diz respeito ao ASP.NET, você não tem escolha - ele usa um modelo assíncrono, porque é isso que faz sentido para o modelo servidor-cliente. Se você estivesse escrevendo internamente seu próprio código que usa um padrão assíncrono para tentar escalar melhor, a menos que esteja tentando gerenciar um recurso que é compartilhado entre todas as solicitações, você não verá melhorias, pois elas já estão agrupadas em um processo assíncrono que não bloqueia mais nada.
Por fim, tudo é subjetivo até que você realmente veja o que está causando um gargalo no seu sistema. Às vezes, é óbvio onde um padrão assíncrono ajudará (impedindo o bloqueio de recursos na fila). Por fim, apenas a medição e a análise de um sistema podem indicar onde você pode obter eficiências.
Editar:
No seu exemplo, a
Task.Factory.StartNew
chamada enfileirará uma operação no pool de threads .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 é concluída, o encadeamento é liberado de volta ao pool para ser reutilizado por outra solicitação (o Garbage Collector na verdade não se envolve, a menos que você tenha criado alguns objetos em suas operações. Nesse caso, eles são coletados normalmente escopo).No que diz respeito ao ASP.NET, não há operação especial aqui. A solicitação do ASP.NET é concluída sem considerar a tarefa assíncrona. A única preocupação pode ser se o pool de threads estiver saturado (por exemplo, não há threads disponíveis para atender à solicitação no momento e as configurações do pool não permitirem a criação de mais threads); nesse caso, a solicitação será bloqueada aguardando o início do tarefa até que um encadeamento de pool fique disponível.
fonte
Task.Factory.StartNew
chamada enfileirará uma operação no pool de threads .NET. . Nesse contexto, qual é o correto aqui: 1-) Ele cria um novo encadeamento e, quando concluído, o encadeamento volta ao conjunto de encadeamentos e espera que seja reutilizado novamente. 2-) Ele obtém um thread do pool de threads e esse thread volta ao threadpool e espera que seja reutilizado novamente. 3-) Adota a abordagem mais eficiente e pode fazer qualquer uma delas.Sim, eles usam um thread do pool de threads. Na verdade, existe um guia excelente do MSDN que abordará todas as suas perguntas e muito mais. Eu achei que era bastante útil no passado. Confira!
http://msdn.microsoft.com/en-us/library/ee728598.aspx
Enquanto isso, os comentários + sugestões que você ouve sobre código assíncrono devem ser tomados com um pouco de sal. Para iniciantes, apenas fazer algo assíncrono não necessariamente aumenta sua escala e, em alguns casos, pode piorar a escala de seu aplicativo. O outro comentário que você publicou sobre "um enorme volume de tráfego ..." também está correto apenas em determinados contextos. Realmente depende do que suas operações estão fazendo e de como elas interagem com outras partes do sistema.
Em resumo, muitas pessoas têm muitas opiniões sobre assíncrono, mas elas podem não estar corretas fora de contexto. Eu diria que se concentre nos seus problemas exatos e faça testes básicos de desempenho para ver o que os controladores assíncronos etc. realmente lidam com o seu aplicativo.
fonte
GC
vem e os limpa logo após o término, para que meu aplicativo não tenha vazamento de memória, etc. Alguma idéia dessa parte.A primeira coisa que não é o MVC, mas o IIS que mantém o pool de threads. Portanto, qualquer solicitação que chega ao aplicativo MVC ou ASP.NET é atendida a partir de threads mantidos no pool de threads. Somente ao criar o aplicativo Asynch, ele invoca essa ação em um thread diferente e libera o thread imediatamente para que outras solicitações possam ser atendidas.
Expliquei o mesmo com um vídeo detalhado ( http://www.youtube.com/watch?v=wvg13n5V0V0/ "Controladores MVC assíncronos e falta de thread") que mostra como a falta de thread acontece no MVC e como é minimizada usando o MVC Controladores assíncronos. Também medi as filas de solicitação usando perfmon, para que você possa ver como as filas de solicitação são reduzidas para a sincronização MVC e como é pior para as operações de sincronização.
fonte