Usando ThreadPool.QueueUserWorkItem no ASP.NET em um cenário de alto tráfego

111

Sempre tive a impressão de que usar o ThreadPool para tarefas em segundo plano (digamos não críticas) de curta duração era considerado uma prática recomendada, mesmo em ASP.NET, mas então me deparei com este artigo que parece sugerir o contrário - o argumento sendo que você deve deixar o ThreadPool para lidar com solicitações relacionadas ao ASP.NET.

Então, aqui está como tenho feito pequenas tarefas assíncronas até agora:

ThreadPool.QueueUserWorkItem(s => PostLog(logEvent))

E o artigo sugere, em vez disso, criar um tópico explicitamente, semelhante a:

new Thread(() => PostLog(logEvent)){ IsBackground = true }.Start()

O primeiro método tem a vantagem de ser gerenciado e limitado, mas existe o potencial (se o artigo estiver correto) de que as tarefas em segundo plano estejam disputando threads com manipuladores de solicitações ASP.NET. O segundo método libera o ThreadPool, mas ao custo de ser ilimitado e, portanto, potencialmente usar muitos recursos.

Portanto, minha pergunta é: o conselho do artigo está correto?

Se seu site estava recebendo tanto tráfego que sua ThreadPool estava ficando cheia, então é melhor sair da banda, ou uma ThreadPool cheia implicaria que você está chegando ao limite de seus recursos de qualquer maneira, nesse caso você não deveria tentar iniciar seus próprios tópicos?

Esclarecimento: estou apenas perguntando no escopo de pequenas tarefas assíncronas não críticas (por exemplo, registro remoto), itens de trabalho não caros que exigiriam um processo separado (nesses casos, concordo que você precisará de uma solução mais robusta).

Michael Hart
fonte
O enredo se complica - encontrei este artigo ( blogs.msdn.com/nicd/archive/2007/04/16/… ), que não consigo decifrar. Por um lado, parece estar dizendo que o IIS 6.0+ sempre processa solicitações em threads de trabalho do pool de threads (e que as versões anteriores podem fazer isso), mas há isto: "No entanto, se você estiver usando novas páginas assíncronas do .NET 2.0 ( Async = "true") ou ThreadPool.QueueUserWorkItem (), então a parte assíncrona do processamento será feita dentro de [um thread de porta de conclusão]. " A parte assíncrona do processamento ?
Jeff Sternal
Outra coisa - deve ser fácil de testar em uma instalação do IIS 6.0+ (que eu não tenho agora) inspecionando se os threads de trabalho disponíveis do pool de threads são menores do que seus threads de trabalho máximos e, em seguida, fazendo o mesmo dentro da fila Itens de trabalho.
Jeff Sternal

Respostas:

104

Outras respostas aqui parecem estar deixando de fora o ponto mais importante:

A menos que você esteja tentando paralelizar uma operação com uso intensivo de CPU para fazê-la mais rapidamente em um site de baixa carga, não há nenhum ponto em usar um thread de trabalho.

Isso vale para threads livres, criados por new Thread(...), e threads de trabalho no ThreadPoolque respondem às QueueUserWorkItemsolicitações.

Sim, é verdade, você pode privar o ThreadPoolprocesso em um ASP.NET enfileirando muitos itens de trabalho. Isso impedirá que o ASP.NET processe outras solicitações. As informações no artigo são precisas a esse respeito; o mesmo pool de threads usado para QueueUserWorkItemtambém é usado para atender a solicitações.

Mas se você está realmente enfileirando itens de trabalho suficientes para causar essa privação, então você deve estar privando o pool de threads! Se você estiver executando literalmente centenas de operações com uso intensivo de CPU ao mesmo tempo, de que adiantaria ter outro thread de trabalho para atender a uma solicitação ASP.NET, quando a máquina já está sobrecarregada? Se você está passando por essa situação, precisa redesenhar completamente!

Na maioria das vezes, vejo ou ouço falar de código multithread sendo usado de maneira inadequada no ASP.NET, não é para enfileirar trabalhos com uso intensivo de CPU. É para enfileirar o trabalho vinculado a E / S. E se você quiser fazer o trabalho de E / S, deve usar um thread de E / S (Porta de Conclusão de E / S).

Especificamente, você deve usar os retornos de chamada assíncronos suportados por qualquer classe de biblioteca que estiver usando. Esses métodos são sempre identificados de forma muito clara; eles começam com as palavras Begine End. Como em Stream.BeginRead, Socket.BeginConnect, WebRequest.BeginGetResponsee assim por diante.

Estes métodos fazem usar o ThreadPool, mas eles usam IOCPs, que não não interferem com os pedidos do ASP.NET. Eles são um tipo especial de thread leve que pode ser "ativado" por um sinal de interrupção do sistema de E / S. E em um aplicativo ASP.NET, você normalmente tem um thread de E / S para cada thread de trabalho, portanto, cada solicitação pode ter uma operação assíncrona enfileirada. Isso significa literalmente centenas de operações assíncronas sem qualquer degradação significativa do desempenho (supondo que o subsistema de E / S possa acompanhar). É muito mais do que você precisa.

Apenas tenha em mente que os delegados assíncronos não funcionam dessa maneira - eles acabarão usando um thread de trabalho, assim como ThreadPool.QueueUserWorkItem. Somente os métodos assíncronos internos das classes da biblioteca do .NET Framework são capazes de fazer isso. Você pode fazer isso sozinho, mas é complicado e um pouco perigoso e provavelmente está além do escopo desta discussão.

A melhor resposta a essa pergunta, em minha opinião, é não usar o ThreadPool ou uma Threadinstância de plano de fundo no ASP.NET . Não é como criar um thread em um aplicativo Windows Forms, onde você faz isso para manter a IU responsiva e não se preocupa com sua eficiência. No ASP.NET, sua preocupação é a taxa de transferência , e todo esse contexto alternando todos os threads de trabalho irá absolutamente matar sua taxa de transferência, quer você use ThreadPoolou não.

Por favor, se você está escrevendo um código de threading em ASP.NET - considere se ele pode ou não ser reescrito para usar métodos assíncronos pré-existentes, e se não puder, então considere se você realmente precisa ou não do código para ser executado em um thread em segundo plano. Na maioria dos casos, você provavelmente estará adicionando complexidade sem nenhum benefício líquido.

Aaronaught
fonte
Obrigado por essa resposta detalhada e você está certo, eu tento usar métodos assíncronos quando possível (juntamente com controladores assíncronos em ASP.NET MVC). No caso do meu exemplo, com o logger remoto, isso é exatamente o que posso fazer. É um problema de design interessante porque empurra o manuseio assíncrono para o nível mais baixo de seu código (ou seja, a implementação do logger), em vez de ser capaz de decidir, digamos, do nível do controlador (no último caso , você precisaria, por exemplo, de duas implementações de logger para poder escolher).
Michael Hart,
@Michael: Callbacks assíncronos são geralmente muito fáceis de embrulhar se você quiser subir mais níveis; você pode criar uma fachada em torno dos métodos assíncronos e envolvê-los com um único método que usa um Action<T>como retorno de chamada, por exemplo. Se você quer dizer que a escolha de usar um thread de trabalho ou thread de E / S ocorre no nível mais baixo, isso é intencional; apenas esse nível pode decidir se precisa ou não de um IOCP.
Aaronaught
Embora, como ponto de interesse, seja apenas o .NET ThreadPoolque limita você dessa forma, provavelmente porque eles não confiaram nos desenvolvedores para fazer isso direito. O Windows Thread Pool não gerenciado possui uma API muito semelhante, mas na verdade permite que você escolha o tipo de thread.
Aaronaught,
2
Portas de conclusão de E / S (IOCP). a descrição do IOCP não está totalmente correta. no IOCP, você tem um número estático de threads de trabalho que se revezam trabalhando em TODAS as tarefas pendentes. não deve ser confundido com conjuntos de threads que podem ser fixos ou dinâmicos em tamanho, MAS têm um thread por tarefa - escala terrivelmente. ao contrário do ASYNC, você não tem um encadeamento por tarefa. um thread IOCP pode funcionar um pouco na tarefa 1, depois passar para a tarefa 3, tarefa 2 e voltar para a tarefa 1 novamente. os estados da sessão da tarefa são salvos e passados ​​entre os threads.
MickyD
1
E quanto às inserções de banco de dados? Existe um comando ASYNC SQL (como Executar)? As inserções de banco de dados são as operações de E / S mais lentas existentes (por causa do bloqueio) e ter o thread principal aguardando a (s) linha (s) a serem inseridas é apenas uma perda de ciclos da CPU.
Ian Thompson
45

De acordo com Thomas Marquadt da equipe ASP.NET da Microsoft, é seguro usar o ASP.NET ThreadPool (QueueUserWorkItem).

Do artigo :

P) Se meu aplicativo ASP.NET usar threads CLR ThreadPool, não irei privar o ASP.NET, que também usa CLR ThreadPool para executar solicitações? ..

A) Para resumir, não se preocupe em privar o ASP.NET de threads, e se você achar que há um problema aqui, me avise e nós cuidaremos disso.

Q) Devo criar meus próprios tópicos (novo Tópico)? Isso não seria melhor para o ASP.NET, já que ele usa o CLR ThreadPool.

A) Por favor, não. Ou dito de outra forma, não !!! Se você for realmente inteligente - muito mais inteligente do que eu - você pode criar seus próprios tópicos; caso contrário, nem pense nisso. Aqui estão alguns motivos pelos quais você não deve criar novos threads com frequência:

  1. É muito caro, em comparação com QueueUserWorkItem ... A propósito, se você pode escrever um ThreadPool melhor do que o CLR, eu o encorajo a se candidatar a um emprego na Microsoft, porque estamos definitivamente procurando pessoas como você !.
Tim P.
fonte
4

Os sites não devem gerar tópicos.

Normalmente, você move essa funcionalidade para um serviço do Windows com o qual se comunica (eu uso o MSMQ para falar com eles).

- Editar

Descrevi uma implementação aqui: Processamento em segundo plano baseado em fila no aplicativo da Web ASP.NET MVC

- Editar

Para expandir por que isso é ainda melhor do que apenas tópicos:

Usando o MSMQ, você pode se comunicar com outro servidor. Você pode gravar em uma fila entre máquinas, portanto, se determinar, por algum motivo, que sua tarefa em segundo plano está usando demais os recursos do servidor principal, você pode simplesmente deslocá-la de maneira bastante trivial.

Também permite que você processe em lote qualquer tarefa que esteja tentando fazer (enviar e-mails / qualquer outra coisa).

Seda do meio-dia
fonte
4
Eu não concordaria que esta declaração geral seja sempre verdadeira - especialmente para tarefas não críticas. Criar um serviço do Windows, apenas para fins de registro assíncrono, definitivamente parece um exagero. Além disso, essa opção nem sempre está disponível (ser capaz de implantar MSMQ e / ou um serviço do Windows).
Michael Hart
Claro, mas é a maneira 'padrão' de implementar tarefas assíncronas de um site (tema de fila em relação a algum outro processo).
Noon Silk
2
Nem todas as tarefas assíncronas são criadas iguais, é por isso que, por exemplo, existem páginas assíncronas no ASP.NET. Se eu quiser buscar um resultado de um serviço da Web remoto para exibir, não farei isso por meio do MSMQ. Nesse caso, estou escrevendo para um log usando uma postagem remota. Não se encaixa no problema de escrever um serviço do Windows, nem conectar o MSMQ para isso (e nem posso, pois este aplicativo específico está no Azure).
Michael Hart
1
Considere: você está escrevendo para um host remoto? E se esse host estiver inativo ou inacessível? Você vai querer tentar escrever novamente? Talvez sim, talvez não. Com sua implementação, é difícil tentar novamente. Com o serviço, torna-se bastante trivial. Agradeço que você não seja capaz de fazer isso, e vou deixar outra pessoa responder aos problemas específicos com a criação de tópicos de sites [ou seja, se o seu tópico não era de fundo, etc], mas estou delineando o 'apropriado' maneira de fazer isso. Não estou familiarizado com o azure, embora tenha usado o ec2 (você pode instalar um sistema operacional nele, então tudo está bem).
Noon Silk
@silky, obrigado pelos comentários. Eu disse "não crítico" para evitar essa solução mais pesada (porém durável). Esclareci a questão para que fique claro que não estou pedindo as melhores práticas para itens de trabalho em fila. O Azure dá suporte a esse tipo de cenário (ele tem seu próprio armazenamento de fila) - mas a operação de enfileiramento é muito cara para o log síncrono, então eu precisaria de uma solução assíncrona de qualquer maneira. No meu caso, estou ciente das armadilhas da falha, mas não vou adicionar mais infraestrutura apenas no caso de este provedor de log específico falhar - tenho outros provedores de log também.
Michael Hart
4

Eu definitivamente acho que a prática geral para trabalho assíncrono rápido e de baixa prioridade no ASP.NET seria usar o pool de threads do .NET, especialmente para cenários de alto tráfego, pois você deseja que seus recursos sejam limitados.

Além disso, a implementação do threading está oculta - se você começar a gerar seus próprios threads, também terá que gerenciá-los adequadamente. Não estou dizendo que você não poderia fazer isso, mas por que reinventar essa roda?

Se o desempenho se tornar um problema e você puder estabelecer que o pool de threads é o fator limitante (e não as conexões de banco de dados, conexões de rede de saída, memória, tempos limite de página, etc.), você ajusta a configuração do pool de threads para permitir mais threads de trabalho, solicitações em fila de espera etc.

Se você não tiver um problema de desempenho, escolher gerar novos threads para reduzir a contenção com a fila de solicitações ASP.NET é uma otimização prematura clássica.

O ideal é que você não precise usar um thread separado para fazer uma operação de registro - apenas habilite o thread original para concluir a operação o mais rápido possível, que é onde o MSMQ e um thread / processo de consumidor separado entram em cena. Eu concordo que isso é mais pesado e mais trabalhoso para implementar, mas você realmente precisa da durabilidade aqui - a volatilidade de uma fila compartilhada na memória irá rapidamente se desgastar.

Sam
fonte
2

Você deve usar QueueUserWorkItem e evitar a criação de novos encadeamentos como faria para evitar a praga. Para obter um visual que explique por que você não vai matar de fome o ASP.NET, já que ele usa o mesmo ThreadPool, imagine um malabarista muito habilidoso usando duas mãos para manter meia dúzia de pinos de boliche, espadas ou o que quer que seja em vôo. Para ter uma ideia de por que criar seus próprios fios é ruim, imagine o que acontece em Seattle na hora do rush, quando rampas de entrada muito usadas para a rodovia permitem que os veículos entrem no trânsito imediatamente em vez de usar um semáforo e limitar o número de entradas a uma a cada poucos segundos . Finalmente, para uma explicação detalhada, consulte este link:

http://blogs.msdn.com/tmarq/archive/2010/04/14/performing-asynchronous-work-or-tasks-in-asp-net-applications.aspx

Obrigado Thomas

Thomas
fonte
Esse link é muito útil, obrigado por isso Thomas. Também estou interessado em saber o que você acha da resposta de @Aaronaught.
Michael Hart,
Eu concordo com Aaronaught, e disse o mesmo em meu blog. Eu coloquei desta forma: "Em uma tentativa de simplificar esta decisão, você só deve mudar [para outro thread] se, de outra forma, bloquear o thread de solicitação ASP.NET enquanto não faz nada. Esta é uma simplificação exagerada, mas estou tentando para tornar a decisão simples. " Em outras palavras, não faça isso para trabalho computacional sem bloqueio, mas faça-o se estiver fazendo solicitações de serviço da Web assíncronas para um servidor remoto. Ouça Aaronaught! :)
Thomas,
1

Esse artigo não está correto. ASP.NET tem seu próprio pool de threads, threads de trabalho gerenciados, para atender a solicitações ASP.NET. Esse pool geralmente tem algumas centenas de threads e é separado do pool ThreadPool, que é um múltiplo menor de processadores.

O uso de ThreadPool no ASP.NET não interfere nos threads de trabalho do ASP.NET. Usar ThreadPool é bom.

Também seria aceitável configurar um único encadeamento que fosse apenas para registrar mensagens e usar o padrão produtor / consumidor para passar mensagens de log para esse encadeamento. Nesse caso, como o encadeamento tem longa duração, você deve criar um único encadeamento novo para executar o registro.

Usar um novo tópico para cada mensagem é definitivamente um exagero.

Outra alternativa, se você estiver falando apenas sobre registro, é usar uma biblioteca como o log4net. Ele lida com o log em um thread separado e cuida de todos os problemas de contexto que podem surgir nesse cenário.

Samuel Neff
fonte
1
@ Sam, estou usando log4net e não estou vendo os logs sendo escritos em um thread separado - há algum tipo de opção que eu preciso habilitar?
Michael Hart,
1

Eu diria que o artigo está errado. Se você estiver executando uma grande loja .NET, você pode usar o pool com segurança em vários aplicativos e sites (usando pools de aplicativos separados), simplesmente com base em uma instrução na documentação ThreadPool :

Existe um pool de threads por processo. O pool de threads tem um tamanho padrão de 250 threads de trabalho por processador disponível e 1000 threads de conclusão de E / S. O número de threads no pool de threads pode ser alterado usando o método SetMaxThreads. Cada thread usa o tamanho de pilha padrão e é executado na prioridade padrão.

Christopher Nobles
fonte
Um aplicativo em execução em um único processo é totalmente capaz de se desligar! (Ou pelo menos degradar seu próprio desempenho o suficiente para tornar o pool de threads uma proposição perdida.)
Jeff Sternal
Portanto, estou supondo que as solicitações ASP.NET usam os threads de conclusão de E / S (em oposição aos threads de trabalho) - correto?
Michael Hart,
Do artigo de Fritz Onion, vinculei minha resposta: "Este paradigma muda [do IIS 5.0 para o IIS 6.0] a maneira como as solicitações são tratadas no ASP.NET. Em vez de enviar solicitações de inetinfo.exe para o processo de trabalho do ASP.NET, http. sys enfileira diretamente cada solicitação no processo apropriado. Assim, todas as solicitações agora são atendidas por threads de trabalho retirados do pool de threads do CLR e nunca em threads de E / S. " (ênfase minha)
Jeff Sternal,
Hmmm, ainda não tenho certeza ... Esse artigo é de junho de 2003. Se você leu este de maio de 2004 (reconhecidamente ainda muito antigo), ele diz "A página de teste Sleep.aspx pode ser usada para manter um ASP .NET I / O thread busy ", onde Sleep.aspx apenas faz com que o thread em execução atual entre em suspensão: msdn.microsoft.com/en-us/library/ms979194.aspx - Quando eu tiver a chance, verei se eu conseguir codificar esse exemplo e testar no IIS 7 e .NET 3.5
Michael Hart,
Sim, o texto desse parágrafo é confuso. Mais adiante nessa seção, ele se vincula a um tópico de suporte ( support.microsoft.com/default.aspx?scid=kb;EN-US;816829 ) que esclarece as coisas: a execução de solicitações em threads de conclusão de E / S era um .NET Framework 1.0 problema que foi corrigido no Hotfix Rollup Package ASP.NET 1.1 de junho de 2003 (após o qual "TODAS as solicitações agora são executadas em threads de trabalho"). Mais importante ainda, esse exemplo mostra claramente que o pool de threads do ASP.NET é o mesmo pool de threads exposto por System.Threading.ThreadPool.
Jeff Sternal,
1

Eu fui questionado sobre uma pergunta semelhante no trabalho na semana passada e darei a você a mesma resposta. Por que você está usando aplicativos da web multi-threading por solicitação? Um servidor web é um sistema fantástico, altamente otimizado para fornecer muitas solicitações em tempo hábil (isto é, multi-threading). Pense no que acontece quando você solicita quase todas as páginas da web.

  1. Um pedido é feito para alguma página
  2. Html é servido de volta
  3. O Html diz ao cliente para fazer outras solicitações (js, css, imagens, etc.)
  4. Mais informações são fornecidas de volta

Você dá o exemplo de registro remoto, mas isso deve ser uma preocupação do seu logger. Um processo assíncrono deve estar em vigor para receber mensagens em tempo hábil. Sam até aponta que seu logger (log4net) já deve suportar isso.

Sam também está correto ao dizer que usar o Thread Pool no CLR não causará problemas com o thread pool no IIS. A coisa a se preocupar aqui, porém, é que você não está gerando threads de um processo, você está gerando novos threads de threads de pool de threads do IIS. Existe uma diferença e a distinção é importante.

Threads vs Processo

Ambos os threads e processos são métodos de paralelizar um aplicativo. No entanto, os processos são unidades de execução independentes que contêm suas próprias informações de estado, usam seus próprios espaços de endereço e apenas interagem entre si por meio de mecanismos de comunicação entre processos (geralmente gerenciados pelo sistema operacional). Os aplicativos são normalmente divididos em processos durante a fase de design, e um processo mestre explicitamente gera subprocessos quando faz sentido separar logicamente a funcionalidade significativa do aplicativo. Em outras palavras, os processos são uma construção arquitetônica.

Em contraste, um thread é uma construção de codificação que não afeta a arquitetura de um aplicativo. Um único processo pode conter vários threads; todos os threads dentro de um processo compartilham o mesmo estado e o mesmo espaço de memória e podem se comunicar diretamente uns com os outros, porque compartilham as mesmas variáveis.

Fonte

Ty.
fonte
3
@Ty, obrigado pela contribuição, mas estou bem ciente de como funciona um servidor web e não é realmente relevante para a questão - novamente, como eu disse na pergunta, não estou pedindo orientação sobre isso como um questão. Estou pedindo informações técnicas específicas. Quanto a ser "a preocupação do logger" que já deve ter um processo assíncrono em vigor - como você acha que o processo assíncrono deve ser escrito pela implementação do logger?
Michael Hart,
0

Não concordo com o artigo referenciado (C # feeds.com). É fácil criar um novo tópico, mas perigoso. O número ideal de threads ativos a serem executados em um único núcleo é surpreendentemente baixo - menos de 10. É muito fácil fazer com que a máquina perca tempo trocando threads se os threads forem criados para tarefas menores. Threads são recursos que REQUEREM gerenciamento. A abstração WorkItem existe para lidar com isso.

Há uma troca aqui entre reduzir o número de threads disponíveis para solicitações e criar muitos threads para permitir que qualquer um deles seja processado com eficiência. Esta é uma situação muito dinâmica, mas acho que deve ser gerenciada ativamente (neste caso pelo pool de threads) em vez de deixar para o processador ficar à frente da criação de threads.

Finalmente, o artigo faz algumas afirmações bem abrangentes sobre os perigos de usar o ThreadPool, mas realmente precisa de algo concreto para fazer o backup.

Timbó
fonte
0

Parece difícil obter uma resposta definitiva para saber se o IIS usa ou não o mesmo ThreadPool para lidar com solicitações de entrada. Portanto, parece uma boa ideia não usar threads de ThreadPool excessivamente, para que o IIS tenha muitos deles disponíveis. Por outro lado, criar seu próprio thread para cada pequena tarefa parece uma má ideia. Presumivelmente, você tem algum tipo de bloqueio em seu registro, então apenas um thread poderia progredir por vez, e o resto se revezaria sendo programado e não programado (para não mencionar a sobrecarga de gerar um novo thread). Essencialmente, você se depara com os problemas exatos que o ThreadPool foi projetado para evitar.

Parece que um compromisso razoável seria seu aplicativo alocar um único thread de registro para o qual você pudesse passar mensagens. Você deve ter cuidado para que o envio de mensagens seja o mais rápido possível, para não deixar seu aplicativo lento.

bmm6o
fonte
0

Você pode usar Parallel.For ou Parallel.ForEach e definir o limite de encadeamentos possíveis que deseja alocar para funcionar sem problemas e evitar a privação de pool.

No entanto, sendo executado em segundo plano, você precisará usar o estilo TPL puro abaixo no aplicativo da web ASP.Net.

var ts = new CancellationTokenSource();
CancellationToken ct = ts.Token;

ParallelOptions po = new ParallelOptions();
            po.CancellationToken = ts.Token;
            po.MaxDegreeOfParallelism = 6; //limit here

 Task.Factory.StartNew(()=>
                {                        
                  Parallel.ForEach(collectionList, po, (collectionItem) =>
                  {
                     //Code Here PostLog(logEvent);
                  }
                });
Khayam
fonte