Usar mais threads é mais lento?
Muita gente acredita que usar mais threads em um software em 100% dos casos o deixará mais rápido, o que não está certo. Mas do outro lado da moeda existem pessoas que acreditam que usar mais threads é "mais lento" dando a explicação de que você basicamente está dividindo a mesma capacidade de CPU para duas threads.
As duas percepções estão erradas e o primeiro erro é o causador do segundo. Acontece que o uso de múltiplas threads é benéfico para a performance de um software quando você as usa para uma tarefa onde a ordem de execução não importa.
Um exemplo disso é quando você precisa fazer o mesmo processamento de dados em dois ou mais arquivos ao mesmo tempo. Como cada arquivo tem um processamento individual e não importa a ordem em que você os processa, você pode se beneficiar do uso de múltiplas threads para processar múltiplos arquivos ao mesmo tempo.
Mas um desenvolvedor que não entenda isso e acredita que o uso de threads sempre deixa tudo mais rápido, pode acabar tentando usar threads para executar uma tarefa em que a ordem de execução importa. Que seria o caso de tarefas sequenciais, onde o próximo passo depende da finalização do passo anterior.
O problema de tentar usar threads para resolver tarefas sequenciais é que você vai precisar fazer a sincronização das threads, fazendo com que com frequência as threads fiquem em estado ocioso. Ou seja, desse jeito fazendo com o que o software perca performance ao invés de ganhar.
Como ocorrem vários casos de desenvolvedores tentarem "otimizar" software forçando o uso de threads onde não faziam sentido e, por consequência, prejudicando a performance, surgiu então outra percepção errada do motivo desta perda de performance.
Algumas pessoas acreditam que a perda de performance ocorre porque a CPU tem uma capacidade N
que é dividida igualmente com todas as threads. Então se você tem uma thread, você tem N/1
para a sua thread e se você tem duas threads, você tem N/2
para cada uma das 2 threads. Ou seja: N/2 * 2 = N
... Daí a percepção de que "não muda nada" e que isso apenas torna o software "mais lento" já que agora você adiciona a complexidade de ficar trocando a execução entre as threads.
Mas essa percepção está tecnicamente errada porque ela não condiz com como softwares funcionam na vida real. Isso estaria correto apenas no cenário imaginário de uma CPU single-core rodando um único processo, mas na vida real as coisas são muito mais complexas do que isso.
Na vida real seu software roda sobre um sistema operacional e compartilha recursos com milhares de threads de outros processo em execução. Digamos que hajam 100 threads ao todo no sistema e 1 para o seu software, o que significa que o seu software usa 1/100
da capacidade do processador. Adicione mais uma thread e agora seu sofware usa 2/101
da capacidade do processador.
Não é o dobro de performance como a galera que não entende de concorrência acha que é, mas existe de fato um ganho de performance (2/101 > 1/100
). Não é a mesma capacidade sendo dividida por dois igual algumas pessoas acreditam. Essa percepção também não leva em consideração multicore, hyper-threading e a priorização de tarefas do kernel.
Sobre este último: Quanto mais CPU uma thread consome, menos ela é priorizada pelo kernel. Logo dividir o trabalho em dois (ou mais) pode evitar que o processo tenha a performance prejudicada pelo escalonador de tarefas.
Se quiser aprender mais sobre performance, eu escrevi um artigo sobre isso aqui: https://www.tabnews.com.br/Silva97/mitos-da-otimizacao-de-codigo
Eu já escrevi sobre isso: https://www.tabnews.com.br/maniero/e-sempre-garantido-que-uma-aplicacao-com-multiplas-threads-rode-mais-rapido-que-usando-uma-unica-thread.
Uma coisa que muita gente acredita é que é necessário de threads para resolver questão de IO e não é, na verdade nem é o correto, apesar de funcionar. Para IO a solução correta é assincronismo, inclusive o sistema operacional tem mecanismos próprios para lidar com isso e está disponível em bibliotecas diversas. Ao mesmo tempo que acham que código assíncrono sempre deixa mais rápido, mas não acontece por si só em código dependente de CPU, ele é bom para IO.
No passado a única solução que existia para IO era threads mesmo, hoje não é assim, mas muita gente aprendeu assim, pega materiais antigos e acha que é necessário ainda.
O uso correto de threads é quando tem muito processamento que pode rodar concorrentemente sem maiores problemas, ou seja, só vale a pena quando oa CPU ficaria ociosa de outra jeito.
Mas para CPU bound geralmente só vale a pena usar a quantidade de threads igual ao número de processadores disponíveis (eventualmente até virtual pode dar algum ganho, mas só em alguns cenários). Porque é desta forma que você pode aproveitar todos os processadores. Se você usar mais threads que o número de processadores começa ficar mais lento porque você paga o custo de troca de contexto de threads que não é barato, por isso algumas linguagens prefeem mecanismo que não dependem de treads diretamente para processamento concorrente, e começa dividir o a capacidade de cada processador, então é só perda.
Já com IO bound isso não acontece porque o proc essador ficaria ocioso nos momentos que está fazendo IO e o uso da thread permite que o processador seja usado e assim ganha tempo, mesmo que não precise usar isso hoje em dia para ter esse ganho, mas é uma forma possível.
O problema do travamento não acontece só em tarefas sequenciais e em algumas sequencias é possível não ter travamento, não é tão simples definir isso, cada algoritmo pode exigir algo diferente.
Então a forma correta de pensar em CPU bound é ter mais threads que processadores, não tem a ver com o fato de ter 1 ou mais processadores, você pode 64 processadores, você não terá 64 vezes mais velocidade tendo 64 threads por causa do custo de administração de threads. Mas fica pior de você tiver mais threads, por exemplo se tiver 128 ficará mais lento que se tiver 64, porque além de não ter ganho porque só tem 64 processadores e eles começam ter sua capacidade dividida, mas também porque a administração fica mais cara por mais trocas de contexto, que inclusive pode afetar o uso de memória, já que pode afetar o cache, e o custo tem potencial de quase inviabilizar o uso em alguns casos. Novamente, não estamos falando de IO bound aqui.
Se o problema for que seu sistema está usando poucas threads e outros sistemas usam muitas, você aumentar a quantidade de threads do seu sistema ajuda o seu sistema, mas piora outras coisas, ou seja, pode não ser uma boa solução. Na verdade esse padrão mostra que algo foi mal planejado, se você precisa de performance no seu sistema ele não deveria concorrer com outros processos, algum dimensionamento está errado, precisa de uma máquina que não atende a demanda, e não é solução uma sistema ferrar os demais. Em uma máquina adequada as outras threads estão dormindo ou em uso muito pequeno.
E quando precisa de mais prioridade o sistema operacional tem maneiras de entregar isso sem ter que criar thrads extras artificialmente para "roubar" processamento dos outros processos, e é mais "educado" fazer desta forma.
Eu gostaria de ver alguma fonte que diga que o kernel dá menos prioridade para quem usa mais.
S2
Farei algo que muitos pedem para aprender a programar corretamente, gratuitamente (não vendo nada, é retribuição na minha aposentadoria) (links aqui no perfil também).
Seu post tem boas intenções e acerta em cheio ao dizer que "as coisas são mais complicadas na vida real", mas a maior parte das explicações está simplesmente equivocada ou simplificada demais.
Por exemplo a ideia que de dividir um problema CPU bound em mais thread o deixaria mais rápido por que o processo teria mais vruntime, um benchmark simples mostrou que não para de pé.