Quando usar o pool de threads em C #? [fechadas]


127

Eu tenho tentado aprender a programação multithread em C # e estou confuso sobre quando é melhor usar um pool de threads vs. criar meus próprios threads. Um livro recomenda o uso de um pool de threads apenas para pequenas tarefas (seja lá o que isso signifique), mas não consigo encontrar nenhuma orientação real. Quais são algumas considerações que você usa ao tomar essa decisão de programação?

Respostas:


47

Se você possui muitas tarefas lógicas que requerem processamento constante e deseja que isso seja feito em paralelo, use o pool + planejador.

Se você precisar executar tarefas relacionadas à IO simultaneamente, como baixar itens de servidores remotos ou acesso ao disco, mas precisar fazer isso uma vez a cada poucos minutos, faça seus próprios threads e mate-os assim que terminar.

Editar: Sobre algumas considerações, uso pools de threads para acesso ao banco de dados, física / simulação, IA (jogos) e para tarefas com script executadas em máquinas virtuais que processam muitas tarefas definidas pelo usuário.

Normalmente, um pool consiste em 2 threads por processador (provavelmente 4 atualmente); no entanto, você pode configurar a quantidade de threads que deseja, se souber quantos precisa.

Editar: O motivo para criar seus próprios threads é por causa de alterações de contexto (é quando os threads precisam ser trocados dentro e fora do processo, junto com a memória). Ter alterações de contexto inúteis, digamos, quando você não estiver usando seus threads, apenas deixá-los descansar como se poderia dizer, pode facilmente ter metade do desempenho do seu programa (digamos que você tenha 3 threads adormecidos e 2 ativos). Portanto, se esses threads de download estão apenas esperando, estão consumindo toneladas de CPU e esfriando o cache do seu aplicativo real


2
Ok, mas você pode explicar por que é assim que você aborda isso? Por exemplo, qual é a desvantagem de usar o pool de threads para fazer o download de servidores remotos ou fazer E / S de disco?

8
Se um encadeamento estiver aguardando um objeto de sincronização (evento, semáforo, mutex, etc.), o encadeamento não consumirá CPU.
285 Brannon Brannon

7
Como Brannon disse, um mito comum é que a criação de vários threads afeta o desempenho. Na verdade, os threads não utilizados consomem muito poucos recursos. Os comutadores de contexto começam a ser um problema apenas em servidores de demanda muito alta (nesse caso, consulte as portas de conclusão de E / S para uma alternativa).
FDCastel

12
Os encadeamentos ociosos afetam o desempenho? Depende de como eles esperam. Se bem escrito e aguardando um objeto de sincronização, eles não devem consumir recursos da CPU. Se a espera em um loop que é ativada periodicamente para verificar os resultados, está desperdiçando a CPU. Como sempre, tudo se resume a uma boa codificação.
Bill Bill

2
Threads gerenciados inativos consomem memória para sua pilha. Por padrão, é 1 MiB por thread. Portanto, é melhor ter todos os threads funcionando.
Vadym Stetsiak

48

Eu sugiro que você use um pool de threads em C # pelos mesmos motivos que em qualquer outro idioma.

Quando você quiser limitar o número de threads em execução ou não desejar criar e destruí-los, use um pool de threads.

Em pequenas tarefas, o livro que você lê significa tarefas com uma vida útil curta. Se levar dez segundos para criar um encadeamento que é executado apenas por um segundo, esse é o local em que você deve usar pools (ignore meus números reais, é a proporção que conta).

Caso contrário, você passa a maior parte do tempo criando e destruindo threads, em vez de simplesmente fazer o trabalho que eles pretendem fazer.


28

Aqui está um bom resumo do pool de threads em .Net: http://blogs.msdn.com/pedram/archive/2007/08/05/dedicated-thread-or-a-threadpool-thread.aspx

A postagem também tem alguns pontos sobre quando você não deve usar o pool de threads e iniciar seu próprio thread.


8
-1 para o link. Tenho certeza de que é um bom link, mas espero que o SO seja auto-suficiente.
Jon Davis

26
@ stimpy77 - essa é a expectativa errada então. O SO nunca pode ser auto-suficiente, porque não é a autoridade suprema em todas as perguntas, nem todas as informações detalhadas sobre cada tópico podem (e devem) ser duplicadas em cada resposta do SO que toque nesse tópico. (e eu não acho que você tenha reputação suficiente para rebaixar todas as respostas de Jon Skeet que possui um link de saída, muito menos de todas as respostas de todos os usuários de SO que possuem links de saída :-))
Franci Penov

2
Talvez eu estivesse sendo muito sucinto, talvez eu devesse esclarecer. Eu não sou contra links. Sou contra as respostas que contêm apenas um link. Eu não acho que isso seja uma resposta. Agora, se uma breve síntese da resposta tivesse sido publicada para resumir como o conteúdo vinculado se aplica, isso seria aceitável. Além disso, eu vim aqui procurando uma resposta para o mesmo problema e essa resposta me irritou porque era mais um link no qual eu precisava clicar para ter uma idéia do que poderia dizer em relação ao problema específico. De qualquer forma, onde Jon Skeet se relaciona com isso? E por que eu deveria me importar?
Jon Davis

8
"Você veio a este post dois anos depois que ele foi publicado e qualquer coisa que eu copiei aqui pode estar obsoleta agora." O mesmo pode acontecer com um link. Publique um resumo sucinto, mas completo, ao publicar um link, você nunca sabe se um link fica obsoleto ou morto.
Jon Davis

2
Eu não concordo com stimpy: não com a idéia de postagens contendo toneladas de informações devido à inviabilidade, nem chamando alguém por isso. Eu diria que é mais provável que um link se torne inoperante do que o conteúdo se torne obsoleto / obsoleto. Portanto, mais conteúdo é bom quando a ocasião permite. Somos todos (principalmente) voluntários, por isso, ser grato para o que você - graças Franci :)
zanlok

14

Eu recomendo a leitura deste e-livro gratuito: Threading in C # por Joseph Albahari

Leia pelo menos a seção "Introdução". O e-book fornece uma ótima introdução e inclui uma riqueza de informações avançadas sobre segmentação.

Saber se deve ou não usar o pool de threads é apenas o começo. Em seguida, você precisará determinar qual método de entrada no pool de threads melhor se adequa às suas necessidades:

  • Biblioteca paralela de tarefas (.NET Framework 4.0)
  • ThreadPool.QueueUserWorkItem
  • Delegados assíncronos
  • BackgroundWorker

Este e-book explica tudo isso e aconselha quando usá-los versus criar seu próprio segmento.


8

O conjunto de encadeamentos foi projetado para reduzir a alternância de contexto entre seus encadeamentos. Considere um processo que possui vários componentes em execução. Cada um desses componentes pode estar criando threads de trabalho. Quanto mais threads em seu processo, mais tempo é desperdiçado na alternância de contexto.

Agora, se cada um desses componentes estivesse enfileirando itens no pool de encadeamentos, você teria muito menos sobrecarga de alternância de contexto.

O conjunto de encadeamentos foi projetado para maximizar o trabalho que está sendo realizado em suas CPUs (ou núcleos de CPU). É por isso que, por padrão, o pool de threads gera vários threads por processador.

Existem algumas situações em que você não deseja usar o pool de threads. Se você estiver aguardando E / S ou aguardando um evento, etc., amarre esse encadeamento do conjunto de encadeamentos e ele não poderá ser usado por mais ninguém. A mesma idéia se aplica a tarefas de longa duração, embora o que constitua uma tarefa de longa duração seja subjetivo.

Pax Diablo também faz questão. Girar tópicos não é gratuito. Leva tempo e eles consomem memória adicional para o espaço de pilha. O conjunto de encadeamentos reutilizará encadeamentos para amortizar esse custo.

Nota: você perguntou sobre o uso de um encadeamento do conjunto de encadeamentos para fazer download de dados ou executar E / S de disco. Você não deve usar um thread de pool de threads para isso (pelas razões descritas acima). Em vez disso, use E / S assíncrona (também conhecida como métodos BeginXX e EndXX). Para um FileStreamque seria BeginReade EndRead. Para um HttpWebRequestque seria BeginGetResponsee EndGetResponse. Eles são mais complicados de usar, mas são a maneira correta de executar E / S multithread.


1
ThreadPool é um automatizador inteligente. "Se sua fila permanecer estacionária por mais de meio segundo, ela responderá criando mais threads - um a cada meio segundo - até a capacidade do pool de threads" ( albahari.com/threading/#_Optimizing_the_Thread_Pool ). Também operações quase assíncronas com BeginXXX-EndXXX são usadas via ThreadPool. Portanto, é normal usar o ThreadPool para baixar dados e frequentemente usado implicitamente.
Artru

6

Cuidado com o pool de encadeamentos .NET para operações que podem bloquear qualquer parte significativa, variável ou desconhecida de seu processamento, pois é propenso a falta de encadeamento. Considere usar as extensões paralelas do .NET, que fornecem um bom número de abstrações lógicas em operações encadeadas. Eles também incluem um novo agendador, que deve ser uma melhoria no ThreadPool. Veja aqui


2
Descobrimos isso da maneira mais difícil! O ASP.Net usa o Threadpool é exibido e, portanto, não podemos usá-lo tão agressivo quanto gostaríamos.
Noócte 16/10/08

3

Um motivo para usar o conjunto de encadeamentos apenas para pequenas tarefas é que há um número limitado de encadeamentos de conjuntos de encadeamentos. Se um for usado por um longo período de tempo, ele interromperá o uso desse segmento por outro código. Se isso acontecer muitas vezes, o conjunto de encadeamentos poderá se esgotar.

Usar o pool de threads pode ter efeitos sutis - alguns timers do .NET usam threads do pool de threads e não são acionados, por exemplo.


2

Se você tiver uma tarefa em segundo plano que permanecerá por muito tempo, como durante toda a vida útil do seu aplicativo, criar um encadeamento próprio é uma coisa razoável. Se você tiver trabalhos curtos que precisem ser executados em um encadeamento, use o pool de encadeamentos.

Em um aplicativo em que você está criando muitos threads, a sobrecarga de criação dos threads se torna substancial. O uso do conjunto de encadeamentos cria os encadeamentos uma vez e os reutiliza, evitando assim a sobrecarga de criação de encadeamentos.

Em um aplicativo em que trabalhei, a alteração da criação de threads para o uso do pool de threads para os threads de curta duração realmente ajudou na aplicação do aplicativo.


Esclareça se você quer dizer "um pool de threads" ou "o pool de threads". Essas são coisas muito diferentes (pelo menos no MS CLR).
Bzlm 01/10/08

2

Para obter o melhor desempenho com unidades em execução simultânea, escreva seu próprio conjunto de encadeamentos, onde um conjunto de objetos Thread é criado na inicialização e vá para o bloqueio (anteriormente suspenso), aguardando a execução de um contexto (um objeto com uma interface padrão implementada por seu código).

Tantos artigos sobre Tarefas versus Threads vs. .NET ThreadPool não fornecem realmente o que você precisa para tomar uma decisão de desempenho. Mas quando você os compara, os Threads vencem e, especialmente, um pool de Threads. Eles são distribuídos da melhor maneira entre as CPUs e iniciam mais rapidamente.

O que deve ser discutido é o fato de que a principal unidade de execução do Windows (incluindo o Windows 10) é um encadeamento e a sobrecarga de alternância de contexto do SO geralmente é insignificante. Simplificando, não consegui encontrar evidências convincentes de muitos desses artigos, se o artigo alega desempenho superior ao salvar a alternância de contexto ou um melhor uso da CPU.

Agora, para um pouco de realismo:

A maioria de nós não precisará que nosso aplicativo seja determinístico, e a maioria de nós não tem um histórico de problemas com threads, o que, por exemplo, geralmente ocorre com o desenvolvimento de um sistema operacional. O que escrevi acima não é para iniciantes.

Então, o que pode ser mais importante é discutir o que é fácil de programar.

Se você criar seu próprio pool de encadeamentos, precisará escrever um pouco, pois precisa se preocupar com o rastreamento do status de execução, como simular a suspensão e o resumo e como cancelar a execução - inclusive em um aplicativo em todo o aplicativo desligar. Você também pode ter que se preocupar se deseja aumentar dinamicamente seu pool e também qual limitação de capacidade seu pool terá. Posso escrever esse quadro em uma hora, mas é porque já o fiz tantas vezes.

Talvez a maneira mais fácil de escrever uma unidade de execução seja usar uma tarefa. A vantagem de uma tarefa é que você pode criar uma e iniciá-la em linha no seu código (embora seja necessário cuidado). Você pode passar um token de cancelamento para manipular quando deseja cancelar a tarefa. Além disso, ele usa a abordagem de promessa para encadear eventos e você pode retornar um tipo específico de valor. Além disso, com async e wait, existem mais opções e seu código será mais portátil.

Em essência, é importante entender os prós e os contras com o Tasks vs. Threads vs. o .NET ThreadPool. Se eu precisar de alto desempenho, usarei threads e prefiro usar meu próprio pool.

Uma maneira fácil de comparar é iniciar 512 threads, 512 tarefas e 512 threads ThreadPool. Você encontrará um atraso no início com Threads (portanto, por que gravar um pool de threads), mas todos os 512 Threads estarão em execução em alguns segundos, enquanto os threads Tasks e .NET ThreadPool demoram alguns minutos para iniciar.

Abaixo estão os resultados desse teste (quad core i5 com 16 GB de RAM), dando a cada 30 segundos para execução. O código executado executa E / S de arquivo simples em uma unidade SSD.

Resultado dos testes


1
Para sua informação, esqueci de mencionar que as Tarefas e os Threads .NET são simulados simultaneidade no .NET e com o gerenciamento em execução no .NET e não no SO - este último sendo muito mais eficiente no gerenciamento de execuções simultâneas. Uso Tarefas para muitas coisas, mas uso um Thread do SO para desempenho de execução pesado. A Microsoft afirma que as tarefas e os threads do .NET são melhores, mas geralmente equilibram a simultaneidade entre aplicativos .NET. Um aplicativo de servidor, no entanto, teria um desempenho melhor, deixando o sistema operacional lidar com a simultaneidade.

Gostaria de ver a implementação do seu Threadpool personalizado. Bom escrever!
Francis

Eu não entendo seus resultados de teste. O que das "Units Ran" significa? Você compara 34 tarefas com 512 threads? Você poderia explicar isso?
Elmue 23/11

Unit é apenas um método para executar simultaneamente em um thread de trabalho Task, Thread ou .NET ThreadPool, meu teste comparando o desempenho de inicialização / execução. Cada teste tem 30 segundos para gerar 512 Threads do zero, 512 Tasks, 512 threads de trabalho ThreadPool ou retomar um conjunto de 512 Threads iniciados aguardando um contexto para execução. Os segmentos de trabalho Tasks e ThreadPool têm uma rotação lenta, portanto, 30 segundos não são tempo suficiente para girar todos eles. No entanto, se a contagem mínima de threads de trabalho do ThreadPool for definida primeiro como 512, os threads de trabalho Tasks e ThreadPool serão girados quase tão rápido quanto 512 Threads do zero.


1

Os conjuntos de encadeamentos são ótimos quando você tem mais tarefas para processar do que os encadeamentos disponíveis.

Você pode adicionar todas as tarefas a um conjunto de threads e especificar o número máximo de threads que podem ser executados em um determinado momento.

Confira esta página no MSDN: http://msdn.microsoft.com/en-us/library/3dasc8as(VS.80).aspx


Ok, acho que isso está vinculado à minha outra pergunta. Como você sabe quantos threads disponíveis você tem em um determinado momento?

Bem, é difícil dizer. Você terá que fazer testes de desempenho. Após um ponto, adicionar mais threads não fornecerá mais velocidade. Descubra quantos processadores existem na máquina, esse será um bom ponto de partida. Em seguida, suba a partir daí, se a velocidade do processamento não melhorar, não adicione mais threads.
lajos

1

Sempre use um conjunto de encadeamentos, se puder, trabalhe no nível mais alto possível de abstração. Os pools de encadeamentos ocultam a criação e destruição de encadeamentos para você; isso geralmente é uma coisa boa!


1

Na maioria das vezes, você pode usar o pool para evitar o processo caro de criar o encadeamento.

No entanto, em alguns cenários, convém criar um thread. Por exemplo, se você não é o único que usa o conjunto de encadeamentos e o encadeamento criado é de longa duração (para evitar consumir recursos compartilhados) ou, por exemplo, se deseja controlar o tamanho da pilha do encadeamento.


1

Não se esqueça de investigar o trabalhador em segundo plano.

Acho que para muitas situações, isso me dá exatamente o que eu quero sem o trabalho pesado.

Felicidades.


quando é um aplicativo simples que continua sendo executado e você tem outra tarefa a executar, é muito fácil executar esse código. você não forneceu links: especificação e tutorial
zanlok 4/10/10

0

Eu normalmente uso o Threadpool sempre que preciso fazer algo em outro thread e realmente não me importo quando ele é executado ou finalizado. Algo como log ou talvez até o download de um arquivo em segundo plano (embora existam maneiras melhores de fazer isso no estilo assíncrono). Uso meu próprio segmento quando preciso de mais controle. Também descobri que usar uma fila Threadsafe (hackear a sua própria) para armazenar "objetos de comando" é bom quando tenho vários comandos nos quais preciso trabalhar em> 1 thread. Portanto, você pode dividir um arquivo Xml e colocar cada elemento em uma fila e, em seguida, ter vários threads trabalhando no processamento desses elementos. Eu escrevi essa fila de volta na uni (VB.net!) Que converti para C #. Incluí-o abaixo sem motivo específico (esse código pode conter alguns erros).

using System.Collections.Generic;
using System.Threading;

namespace ThreadSafeQueue {
    public class ThreadSafeQueue<T> {
        private Queue<T> _queue;

        public ThreadSafeQueue() {
            _queue = new Queue<T>();
        }

        public void EnqueueSafe(T item) {
            lock ( this ) {
                _queue.Enqueue(item);
                if ( _queue.Count >= 1 )
                    Monitor.Pulse(this);
            }
        }

        public T DequeueSafe() {
            lock ( this ) {
                while ( _queue.Count <= 0 )
                    Monitor.Wait(this);

                return this.DeEnqueueUnblock();

            }
        }

        private T DeEnqueueUnblock() {
            return _queue.Dequeue();
        }
    }
}

Alguns problemas com essa abordagem: - As chamadas para DequeueSafe () aguardam até que um item seja EnqueuedSafe (). Considere usar uma das sobrecargas Monitor.Wait () especificando um tempo limite. - Bloquear isso não está de acordo com as práticas recomendadas; em vez disso, crie um campo de objeto somente leitura. - Embora Monitor.Pulse () seja leve, chamá-lo quando a fila contém apenas 1 item seria mais eficiente. - DeEnqueueUnblock () deve verificar preferência a queue.Count> 0. (necessária se forem utilizados Monitor.PulseAll ou esperar o tempo limite)
Craig Nicholson

0

Eu queria que um pool de threads distribuísse o trabalho entre os núcleos com a menor latência possível e que não precisassem funcionar bem com outros aplicativos. Descobri que o desempenho do pool de threads .NET não era tão bom quanto poderia ser. Eu sabia que queria um thread por núcleo, então escrevi minha própria classe de substituto do pool de threads. O código é fornecido como resposta a outra pergunta do StackOverflow aqui .

Quanto à pergunta original, o pool de threads é útil para dividir cálculos repetitivos em partes que podem ser executadas em paralelo (supondo que elas possam ser executadas em paralelo sem alterar o resultado). O gerenciamento manual de threads é útil para tarefas como interface do usuário e IO.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.