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

Eu sempre tive a impressão de que usar o ThreadPool para (digamos, não críticas) tarefas de fundo de curta duração era considerado a melhor prática, mesmo no ASP.NET, mas depois me deparei com este artigo que parece sugerir o contrário. argumento sendo que você deve deixar o ThreadPool para lidar com solicitações relacionadas ao ASP.NET.

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

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

E o artigo sugere, em vez disso, criar um encadeamento 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 competindo por threads com os manipuladores de solicitação do ASP.NET. O segundo método libera o ThreadPool, mas ao custo de ser ilimitado e, portanto, potencialmente usando muitos resources.

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

Se o seu site estava recebendo muito tráfego que o seu ThreadPool estava ficando cheio, então é melhor ir fora da banda, ou seria um ThreadPool completo implica que você está chegando ao limite de seus resources de qualquer maneira, caso em que você não deveria estar tentando iniciar seus próprios tópicos?

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

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 da CPU para fazer isso mais rapidamente em um site de baixa carga, não faz sentido usar um thread de trabalho.

Isso vale para ambos os encadeamentos livres, criados pelos new Thread(...) encadeamentos new Thread(...) e de trabalho no ThreadPool que respondem às solicitações QueueUserWorkItem .

Sim, é verdade, você pode passar fome do ThreadPool em um processo do ASP.NET enfileirando muitos itens de trabalho. Isso impedirá que o ASP.NET processe solicitações adicionais. As informações no artigo são precisas a esse respeito; o mesmo conjunto de encadeamentos usado para QueueUserWorkItem também é usado para atender solicitações.

Mas se você está realmente enfileirando itens de trabalho suficientes para causar essa fome, então você deve estar morrendo de fome na piscina de discussão! 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 do ASP.NET, quando a máquina já está sobrecarregada? Se você está correndo para essa situação, você precisa reformular completamente!

Na maior parte do tempo, vejo ou ouço falar que o código multi-threaded é usado de forma inadequada no ASP.NET, não é para enfileirar trabalho intensivo de CPU. É para enfileirar trabalho vinculado a E / S. E se você quiser fazer o trabalho de E / S, então você deve estar usando um encadeamento de E / S (Porta de Conclusão de E / S).

Especificamente, você deve usar os retornos de chamada asynchronouss suportados por qualquer class de biblioteca que estiver usando. Esses methods são sempre rotulados de maneira muito clara; eles começam com as palavras Begin e End . Como em Stream.BeginRead , Socket.BeginConnect , WebRequest.BeginGetResponse e assim por diante.

Esses methods usam o ThreadPool , mas eles usam IOCPs, que não interferem nas solicitações do ASP.NET. Eles são um tipo especial de thread leve que pode ser “acordado” 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 única pode ter uma operação async enfileirada. Isso é literalmente centenas de operações assíncronas sem qualquer degradação de desempenho significativa (supondo que o subsistema de E / S possa acompanhar). É muito mais do que você precisa.

Apenas tenha em mente que os representantes asynchronouss não funcionam dessa maneira – eles acabam usando um thread de trabalho, exatamente como ThreadPool.QueueUserWorkItem . São apenas os methods asynchronouss incorporados das classs de biblioteca do .NET Framework que são capazes de fazer isso. Você pode fazer isso sozinho, mas é complicado e um pouco perigoso e, provavelmente, além do escopo desta discussão.

A melhor resposta para essa pergunta, na minha opinião, é não usar o ThreadPool ou uma instância Thread segundo plano no ASP.NET . Não é nada como criar um thread em um aplicativo do Windows Forms, onde você faz isso para manter a interface do usuário responsiva e não se importa com a eficiência dela. No ASP.NET, sua preocupação é a taxa de transferência , e todo o contexto que liga todos os threads de trabalho é absolutamente eliminado se você usar o ThreadPool ou não.

Por favor, se você estiver escrevendo código de threading no ASP.NET – considere se ele pode ou não ser reescrito para usar methods asynchronouss pré-existentes, e se não puder, então por favor considere se você realmente precisa ou não do código para executar em um segmento de fundo em tudo. Na maioria dos casos, você provavelmente estará adicionando complexidade para nenhum benefício líquido.

Por Thomas Marquadt da equipe do ASP.NET na Microsoft, é seguro usar o ASP.NET ThreadPool (QueueUserWorkItem).

Do artigo :

Q) Se meu aplicativo ASP.NET usa segmentos CLR ThreadPool, não vou privar o ASP.NET, que também usa o CLR ThreadPool para executar solicitações? Bloco de citação

A) [T] o resumir, não se preocupe com fome ASP.NET de threads, e se você acha que há um problema aqui, deixe-me saber e vamos cuidar disso.

Q) Devo criar meus próprios threads (novo Thread)? Isso não será melhor para o ASP.NET, já que ele usa o ThreadPool do CLR.

A) Por favor não. Ou, para falar de outra maneira, não !!! Se você for realmente esperto – muito mais esperto do que eu – então você pode criar seus próprios tópicos; caso contrário, nem pense nisso. Aqui estão algumas razões pelas quais você não deve criar novos tópicos com freqüência:

1) É muito caro, comparado ao QueueUserWorkItem … A propósito, se você pode escrever um ThreadPool melhor que o CLR, eu o encorajo a se candidatar a um emprego na Microsoft, porque definitivamente estamos procurando por pessoas como você! .

Os sites não devem sair por aí gerando tópicos.

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

– Editar

Eu 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 threads:

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

Ele também permite processar em lote qualquer tarefa que você esteja tentando fazer (enviar e-mails / qualquer coisa).

Eu definitivamente acho que a prática geral para trabalho asynchronous 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 resources sejam limitados.

Além disso, a implementação de threading está oculta – se você começar a gerar seus próprios threads, você deve gerenciá-los apropriadamente também. 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 conexões de database, conexões de rede de saída, memory, timeouts de página, etc.), você ajusta a configuração do pool de threads para permitir mais threads de trabalho, solicitações enfileiradas mais altas etc.

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

Idealmente, você não precisaria usar um thread separado para fazer uma operação de logging – basta habilitar o thread original para concluir a operação o mais rápido possível, que é onde o MSMQ e um thread / processo separados do consumidor entram na imagem. Eu concordo que isso é mais pesado e mais trabalho para implementar, mas você realmente precisa da durabilidade aqui – a volatilidade de uma fila compartilhada na memory irá desgastar rapidamente sua recepção.

Você deve usar QueueUserWorkItem e evitar a criação de novos tópicos como evitaria a praga. Para um visual que explica por que você não vai passar fome no 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 esteja em vôo. Para um visual de por que criar seus próprios tópicos é ruim, imagine o que acontece em Seattle na hora do rush quando as rampas de input usadas para a rodovia permitem que os veículos entrem no trânsito imediatamente em vez de usar uma luz e limitar o número de inputs a cada poucos segundos . Finalmente, para uma explicação detalhada, por favor veja este link:

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

Obrigado Thomas

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

O uso do ThreadPool no ASP.NET não interferirá nos threads de trabalho do ASP.NET. Usando ThreadPool está bem.

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

Usando um novo segmento para cada mensagem é definitivamente um exagero.

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

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

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

Foi-me feita uma pergunta semelhante no trabalho na semana passada e vou dar-lhe a mesma resposta. Por que você usa vários aplicativos da Web por solicitação? Um servidor web é um sistema fantástico otimizado para fornecer muitas solicitações em tempo hábil (ou seja, multi-threading). Pense no que acontece quando você solicita quase todas as páginas da web.

  1. Uma solicitação é feita para alguma página
  2. Html é servido de volta
  3. O Html diz ao cliente para fazer mais requisições (js, css, imagens, etc.)
  4. Mais informações são servidas de volta

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

Sam também está correto, pois usar o pool de threads no CLR não causará problemas com o pool de threads 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 fora dos threads do IIS poolpool. Há uma diferença e a distinção é importante.

Tópicos vs Processo

Ambos os encadeamentos e processos são methods 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 interagem somente entre si por meio de mecanismos de comunicação entre processos (geralmente gerenciados pelo sistema operacional). Os aplicativos normalmente são divididos em processos durante a fase de design, e um processo mestre gera explicitamente subprocesss quando faz sentido separar logicamente a funcionalidade significativa do aplicativo. Processos, em outras palavras, são uma construção arquitetônica.

Por outro lado, um thread é uma construção de codificação que não afeta a arquitetura de um aplicativo. Um único processo pode conter vários segmentos; Todos os threads dentro de um processo compartilham o mesmo estado e o mesmo espaço de memory, e podem se comunicar diretamente entre si, porque compartilham as mesmas variables.

Fonte

Eu não concordo com o artigo referenciado (C # feeds.com). É fácil criar um novo tópico, mas perigoso. O número ideal de encadeamentos ativos a serem executados em um único núcleo é, na verdade, surpreendentemente baixo – menor que 10. É muito fácil fazer com que a máquina perca tempo trocando encadeamentos se os encadeamentos forem criados para tarefas menores. Threads são um recurso que REQUIRE o gerenciamento. A abstração WorkItem está lá para lidar com isso.

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

Finalmente, o artigo faz algumas declarações bastante abrangentes sobre os perigos do uso do ThreadPool, mas ele realmente precisa de algo concreto para apoiá-los.

Se o IIS usa ou não o mesmo ThreadPool para lidar com solicitações de input, parece difícil obter uma resposta definitiva e também parece ter mudado em relação às versões. Portanto, parece uma boa idéia não usar threads ThreadPool excessivamente, para que o IIS tenha muitos deles disponíveis. Por outro lado, gerar o seu próprio tópico para cada pequena tarefa parece ser uma má ideia. Presumivelmente, você tem algum tipo de bloqueio em seu log, portanto, apenas um thread poderia progredir de cada vez, e o resto apenas se revezaria ficando agendado e não agendado (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 para o seu aplicativo alocar um único segmento de log para o qual você poderia passar mensagens. Você deve ter cuidado para que o envio de mensagens seja o mais rápido possível para que você não diminua o ritmo do seu aplicativo.