Pools de threads

Um conjunto de threads é uma coleção de threads de trabalho que executam callbacks assíncronos com eficiência em nome do aplicativo. O pool de threads é usado principalmente para reduzir o número de threads de aplicativos e fornecer gerenciamento dos threads de trabalho. Os aplicativos podem enfileirar itens de trabalho, associar o trabalho a identificadores aguardáveis, enfileirar automaticamente com base em um temporizador e vincular à E/S.

Arquitetura do pool de threads

Os seguintes aplicativos podem se beneficiar do uso de um pool de threads:

  • Um aplicativo altamente paralelo e que pode expedir um grande número de pequenos itens de trabalho de forma assíncrona (como pesquisa de índice distribuído ou E/S de rede).
  • Um aplicativo que cria e destrói um grande número de threads, cada uma executada por pouco tempo. O uso do pool de threads pode reduzir a complexidade do gerenciamento de threads e a sobrecarga envolvida na criação e destruição de threads.
  • Uma aplicação que processa itens de trabalho independentes em segundo plano e em paralelo (como ao carregar várias guias).
  • Um aplicativo que deve executar uma espera exclusiva em objetos do kernel ou bloquear eventos de entrada em um objeto. O uso do pool de threads pode reduzir a complexidade do gerenciamento de threads e aumentar o desempenho, reduzindo o número de alternâncias de contexto.
  • Um aplicativo que cria threads de espera personalizados para aguardar eventos.

O pool de threads original foi completamente reprojetado no Windows Vista. O novo pool de threads foi aprimorado porque fornece um único tipo de thread de trabalho (que oferece suporte tanto a E/S quanto a não E/S), não usa uma thread dedicada a temporizadores, fornece uma única fila de temporizadores e fornece uma thread persistente dedicada. Ele também fornece grupos de limpeza, maior desempenho, vários pools por processo agendados de forma independente e uma nova API de pool de threads.

A arquitetura do pool de threads consiste no seguinte:

  • Threads de trabalho que executam as funções de callback
  • Threads de espera que aguardam vários identificadores de espera
  • Uma fila de trabalho
  • Um pool de threads padrão para cada processo
  • Uma fábrica de trabalhadores que gerencia os threads de trabalho

Práticas Recomendadas

A nova API do pool de threads oferece mais flexibilidade e controle do que a API do pool de threads original. No entanto, há algumas diferenças sutis, mas importantes. Na API original, a redefinição da espera era automática; na nova API, a espera deve ser redefinida explicitamente a cada vez. A API original tratava a representação automaticamente, transferindo o contexto de segurança do processo de chamada para o thread. Com a nova API, o aplicativo deve definir explicitamente o contexto de segurança.

Veja a seguir as práticas recomendadas ao usar um pool de threads:

  • Os threads de um processo compartilham o pool de threads. Uma única thread de trabalho pode executar múltiplas funções de callback, uma de cada vez. Esses threads de trabalho são gerenciados pelo pool de threads. Portanto, não encerre uma thread do pool de threads chamando TerminateThread na thread nem chamando ExitThread em uma função de callback.

  • Uma solicitação de E/S pode ser executada em qualquer thread no pool de threads. O cancelamento de E/S em um thread do conjunto de threads requer sincronização porque a função de cancelamento pode ser executada em um thread diferente daquele que está manipulando a solicitação de E/S, o que pode resultar no cancelamento de uma operação desconhecida. Para evitar isso, sempre forneça a estrutura OVERLAPPED com a qual uma solicitação de E/S foi iniciada ao chamar CancelIoEx para E/S assíncrona ou use sua própria sincronização para garantir que nenhuma outra E/S possa ser iniciada no thread de destino antes de chamar a função CancelSynchronousIo ou CancelIoEx.

  • Limpe todos os recursos criados na função de callback antes de retornar da função. Isso inclui TLS, contextos de segurança, prioridade de thread e registro COM. As funções de callback também devem restaurar o estado da thread antes de retornar.

  • Mantenha os handles de espera e seus objetos associados ativos até que o pool de threads sinalize que terminou de usar o handle.

  • Marque todos os threads que estão aguardando operações demoradas (como liberações de E/S ou limpeza de recursos) para que o conjunto de threads possa alocar novos threads em vez de aguardar por este.

  • Antes de descarregar uma DLL que usa o pool de threads, cancele todos os itens de trabalho, E/S, operações de espera e temporizadores e aguarde a conclusão dos retornos de chamada.

  • Evite deadlocks eliminando dependências entre itens de trabalho e entre callbacks, garantindo que um callback não esteja aguardando a própria conclusão e preservando a prioridade da thread.

  • Não coloque muitos itens na fila muito rapidamente em um processo com outros componentes usando o conjunto de threads padrão. Há um pool de threads padrão por processo, incluindo Svchost.exe. Por padrão, cada pool de threads tem no máximo 500 threads de trabalho. O conjunto de threads tenta criar mais threads de trabalho quando o número de threads de trabalho no estado pronto/em execução deve ser menor que o número de processadores.

  • Evite o modelo de thread único do COM, pois ele é incompatível com o pool de threads. STA cria um estado de thread que pode afetar o próximo item de trabalho do thread. STA geralmente é de longa duração e tem afinidade de thread, o que é o oposto do pool de threads.

  • Crie um pool de threads para controlar a prioridade e o isolamento do thread, criar características personalizadas e possivelmente melhorar a capacidade de resposta. No entanto, pools de threads adicionais requerem mais recursos do sistema (threads, memória de kernel). Número excessivo de pools aumenta o potencial de contenção de CPU.

  • Se possível, use um objeto passível de espera em vez de um mecanismo baseado em APC para sinalizar uma thread do pool de threads. As APCs não funcionam tão bem com as threads em um pool de threads quanto outros mecanismos de sinalização, porque o sistema controla o ciclo de vida das threads do pool de threads, de modo que é possível que uma thread seja encerrada antes que a notificação seja entregue.

  • Use a extensão do depurador do pool de threads, !tp. Este comando tem o seguinte uso:

    • sinalizadores de endereçode pool
    • obj endereçosinalizadores
    • sinalizadores de endereçoda tqueue
    • endereço do garçom
    • endereço do trabalhador

    Para pool, waiter e worker, se o endereço for zero, o comando despeja todos os objetos. Para garçom e trabalhador, a omissão do endereço despeja o thread atual. Os seguintes sinalizadores são definidos: 0x1 (saída em uma única linha), 0x2 (despejar os membros) e 0x4 (despejar a fila de trabalho do pool).

Note

Alternativas modernas: Para aplicativos C++/WinRT, o pool de threads Windows é acessível por meio de coroutines – use co_await winrt::resume_background() para expedir o trabalho para o pool de threads e co_await winrt::resume_foreground(dispatcher) para retornar ao thread da interface do usuário. Isso fornece concorrência estruturada sem a necessidade de gerenciar callbacks manualmente. Para código C++ padrão, std::async com a política std::launch::async oferece paralelismo portátil baseado em tarefas, embora não use diretamente o pool de threads do Windows.

API do pool de threads

Como usar as funções do pool de threads