Ele está vagamente relacionado a esta questão: std :: thread é agrupado em C ++ 11? . Embora a pergunta seja diferente, a intenção é a mesma:
Pergunta 1: ainda faz sentido usar seus próprios pools de thread (ou biblioteca de terceiros) para evitar a criação de thread cara?
A conclusão na outra pergunta foi que você não pode confiar std::thread
para ser agrupado (pode ou não ser). No entanto, std::async(launch::async)
parece ter uma chance muito maior de ser agrupado.
Não acho que seja forçado pelo padrão, mas IMHO eu esperaria que todas as boas implementações do C ++ 11 usassem o pool de threads se a criação de threads fosse lenta. Apenas em plataformas em que é barato criar um novo thread, eu esperaria que eles sempre gerassem um novo thread.
Pergunta 2: Isso é exatamente o que eu penso, mas não tenho fatos para provar isso. Posso muito bem estar enganado. É um palpite?
Finalmente, forneci aqui alguns códigos de amostra que mostram primeiro como acho que a criação de threads pode ser expressa por async(launch::async)
:
Exemplo 1:
thread t([]{ f(); });
// ...
t.join();
torna-se
auto future = async(launch::async, []{ f(); });
// ...
future.wait();
Exemplo 2: disparar e esquecer o tópico
thread([]{ f(); }).detach();
torna-se
// a bit clumsy...
auto dummy = async(launch::async, []{ f(); });
// ... but I hope soon it can be simplified to
async(launch::async, []{ f(); });
Pergunta 3: Você prefere as async
versões às thread
versões?
O resto já não faz parte da questão, mas apenas para esclarecimentos:
Por que o valor de retorno deve ser atribuído a uma variável fictícia?
Infelizmente, o padrão C ++ 11 atual força que você capture o valor de retorno std::async
, caso contrário, o destruidor é executado, o que bloqueia até que a ação termine. É considerado por alguns um erro no padrão (por exemplo, por Herb Sutter).
Este exemplo de cppreference.com ilustra bem:
{
std::async(std::launch::async, []{ f(); });
std::async(std::launch::async, []{ g(); }); // does not run until f() completes
}
Outro esclarecimento:
Eu sei que os pools de thread podem ter outros usos legítimos, mas nesta questão estou interessado apenas no aspecto de evitar custos de criação de thread caros .
Acho que ainda existem situações em que os pools de threads são muito úteis, especialmente se você precisar de mais controle sobre os recursos. Por exemplo, um servidor pode decidir lidar com apenas um número fixo de solicitações simultaneamente para garantir tempos de resposta rápidos e aumentar a previsibilidade do uso de memória. Pools de threads devem estar bem, aqui.
Variáveis de thread local também podem ser um argumento para seus próprios pools de thread, mas não tenho certeza se isso é relevante na prática:
- A criação de um novo encadeamento
std::thread
começa sem variáveis locais de encadeamento inicializadas. Talvez não seja isso que você deseja. - Em tópicos gerados por
async
, não está claro para mim porque o tópico poderia ter sido reutilizado. Do meu entendimento, as variáveis locais de thread não têm garantia de serem redefinidas, mas posso estar enganado. - Usar seus próprios pools de threads (de tamanho fixo), por outro lado, oferece controle total se você realmente precisar dele.
fonte
std::async(launch::async)
parece ter uma chance muito maior de ser agrupado." Não, eu acreditostd::async(launch::async | launch::deferred)
que pode ser agrupado. Com apenaslaunch::async
a tarefa deve ser iniciada em um novo thread, independentemente de quais outras tarefas estão em execução. Com a políticalaunch::async | launch::deferred
, a implementação pode escolher qual política, mas o mais importante, pode atrasar a escolha de qual política. Ou seja, ele pode esperar até que um thread em um pool de threads se torne disponível e, em seguida, escolher a política assíncrona.std::async()
. Ainda estou curioso para ver como eles oferecem suporte a destruidores thread_local não triviais em um pool de threads.launch::async
, ela a trata como se fosse únicalaunch::deferred
e nunca a executa de forma assíncrona - então, na verdade, essa versão de libstdc ++ "escolhe" sempre usar diferido, a menos que forçado de outra forma.std::async
poderia ter sido uma coisa bonita para o desempenho - poderia ter sido o sistema padrão de execução de tarefas curtas, naturalmente apoiado por um pool de threads. No momento, é apenas umstd::thread
com alguma porcaria acrescentada para tornar a função thread ser capaz de retornar um valor. Ah, e eles adicionaram funcionalidade "adiada" redundante que se sobrepõestd::function
completamente ao trabalho .Respostas:
Questão 1 :
Eu mudei isso do original porque o original estava errado. Fiquei com a impressão de que a criação de thread no Linux era muito barata e, depois de testar, determinei que a sobrecarga de chamada de função em uma nova thread em relação a uma normal é enorme. A sobrecarga para criar um thread para lidar com uma chamada de função é algo como 10.000 ou mais vezes mais lenta do que uma chamada de função simples. Portanto, se você estiver emitindo muitas chamadas de funções pequenas, um pool de threads pode ser uma boa ideia.
É bastante evidente que a biblioteca C ++ padrão fornecida com g ++ não tem pools de threads. Mas posso definitivamente ver um caso para eles. Mesmo com a sobrecarga de ter que empurrar a chamada por algum tipo de fila entre threads, provavelmente seria mais barato do que iniciar um novo thread. E o padrão permite isso.
IMHO, o pessoal do kernel Linux deve trabalhar para tornar a criação de threads mais barata do que é atualmente. Mas, a biblioteca C ++ padrão também deve considerar o uso de pool para implementação
launch::async | launch::deferred
.E o OP está correto, usar
::std::thread
para lançar um thread obviamente força a criação de um novo thread em vez de usar um de um pool. Então::std::async(::std::launch::async, ...)
é o preferido.Questão 2 :
Sim, basicamente, isso inicia um tópico 'implicitamente'. Mas, realmente, ainda é bastante óbvio o que está acontecendo. Portanto, não acho que a palavra implicitamente seja uma palavra particularmente boa.
Também não estou convencido de que forçar você a esperar por um retorno antes da destruição seja necessariamente um erro. Não sei se você deve usar a
async
chamada para criar threads de 'daemon' que não devem retornar. E se eles devem retornar, não é normal ignorar exceções.Questão 3 :
Pessoalmente, gosto que os lançamentos de threads sejam explícitos. Eu valorizo muito as ilhas onde você pode garantir acesso serial. Caso contrário, você acabará com o estado mutável, de modo que sempre terá que envolver um mutex em algum lugar e se lembrar de usá-lo.
Gostei muito mais do modelo de fila de trabalho do que do modelo 'futuro' porque há 'ilhas de série' espalhadas para que você possa lidar com o estado mutável de maneira mais eficaz.
Mas, na verdade, depende exatamente do que você está fazendo.
Teste de performance
Portanto, testei o desempenho de vários métodos de chamada de coisas e cheguei a esses números em um sistema de 8 núcleos (AMD Ryzen 7 2700X) executando o Fedora 29 compilado com clang versão 7.0.1 e libc ++ (não libstdc ++):
E nativo, no meu MacBook Pro 15 "(Intel (R) Core (TM) i7-7820HQ CPU @ 2,90 GHz) com
Apple LLVM version 10.0.0 (clang-1000.10.44.4)
OSX 10.13.6, eu obtenho o seguinte:Para o thread de trabalho, iniciei um thread, usei uma fila sem bloqueio para enviar solicitações a outro thread e esperei que uma resposta "Pronto" fosse enviada de volta.
O "Não fazer nada" serve apenas para testar a sobrecarga do equipamento de teste.
É claro que a sobrecarga de lançar um thread é enorme. E mesmo o thread de trabalho com a fila entre threads diminui as coisas em um fator de 20 ou mais no Fedora 25 em uma VM, e cerca de 8 no OS X nativo.
Criei um projeto Bitbucket contendo o código que usei para o teste de desempenho. Ele pode ser encontrado aqui: https://bitbucket.org/omnifarious/launch_thread_performance
fonte