Por que o multithreading geralmente é preferido para melhorar o desempenho?


23

Eu tenho uma pergunta, é sobre por que os programadores parecem gostar de simultaneidade e programas multithread em geral.

Estou considerando duas abordagens principais aqui:

  • uma abordagem assíncrona basicamente baseada em sinais, ou apenas uma abordagem assíncrona chamada por muitos documentos e idiomas como o novo C # 5.0, por exemplo, e um "encadeamento complementar" que gerencia a política do seu pipeline
  • uma abordagem simultânea ou multi-threading

Vou apenas dizer que estou pensando no hardware aqui e no pior cenário, e eu mesmo testei esses 2 paradigmas, o paradigma assíncrono é um vencedor no momento em que não entendo por que as pessoas 90% do tempo fale sobre multithreading quando quiser acelerar as coisas ou fazer um bom uso de seus recursos.

Eu testei programas multithread e assíncronos em uma máquina antiga com um quad-core Intel que não oferece um controlador de memória dentro da CPU; a memória é gerenciada inteiramente pela placa-mãe; nesse caso, os desempenhos são horríveis com um aplicação multithread, mesmo um número relativamente baixo de threads como 3-4-5 pode ser um problema, o aplicativo não responde e é apenas lento e desagradável.

Uma boa abordagem assíncrona é, por outro lado, provavelmente não mais rápida, mas também não é pior, meu aplicativo aguarda o resultado e não trava, é responsivo e existe uma escala muito melhor.

Também descobri que uma mudança de contexto no mundo do encadeamento não é tão barata no cenário do mundo real, é de fato muito cara, especialmente quando você tem mais de 2 encadeamentos que precisam alternar entre si para serem computados.

Nas CPUs modernas, a situação não é tão diferente, o controlador de memória está integrado, mas o que quero dizer é que as CPUs x86 são basicamente uma máquina serial e o controlador de memória funciona da mesma maneira que na máquina antiga com um controlador de memória externo na placa-mãe . A troca de contexto ainda é um custo relevante no meu aplicativo e o fato de o controlador de memória estar integrado ou de que a CPU mais recente possuir mais de 2 núcleos, não é uma pechincha para mim.

Pelo que experimentei, a abordagem simultânea é boa em teoria, mas não tão boa na prática; com o modelo de memória imposto pelo hardware, é difícil fazer um bom uso desse paradigma; também apresenta muitos problemas que vão desde o uso das minhas estruturas de dados para a junção de vários threads.

Além disso, ambos os paradigmas não oferecem nenhum limite de segurança quando a tarefa ou o trabalho será realizado em um determinado momento, tornando-os realmente semelhantes do ponto de vista funcional.

De acordo com o modelo de memória X86, por que a maioria das pessoas sugere usar simultaneidade com C ++ e não apenas uma abordagem assíncrona? Além disso, por que não considerar o pior cenário de um computador em que a troca de contexto provavelmente é mais cara que o próprio cálculo?


2
Uma maneira de comparar seria olhar para o mundo JavaScript, se não houvesse encadeamento e tudo fosse agressivamente assíncrono, usando retornos de chamada. Funciona, mas tem seus próprios problemas.
Gort the Robot

2
@StevenBurnap Como você chama trabalhadores da web?
user16764

2
"mesmo um número relativamente baixo de threads como 3-4-5 pode ser um problema, o aplicativo não responde e é lento e desagradável". => Pode ser devido a um design inadequado / uso inadequado de threads. Normalmente, você encontra esse tipo de situação quando seus encadeamentos continuam trocando dados; nesse caso, a multiencadeamento pode não ser a resposta certa ou pode ser necessário particionar novamente os dados.
Assilias 15/12/12

1
@assylias Ver uma desaceleração significativa no thread da interface do usuário indica uma quantidade excessiva de bloqueios nos threads. Você tem uma implementação ruim ou está tentando enfiar uma estaca quadrada em um buraco redondo.
quer

5
Você diz que "os programadores parecem amar simultaneidade e programas multiencadeados em geral" duvido disso. Eu diria que "os programadores odiá-lo" ... mas muitas vezes é a única coisa útil para fazer ...
Johannes

Respostas:


34

Você tem vários núcleos / procesores, use- os

O Async é melhor para o processamento pesado de IO, mas e o processamento pesado de CPU?

O problema surge quando blocos de código de thread único (ou seja, ficam presos) em um processo de longa execução. Por exemplo, lembre-se de que quando imprimir um documento com processador de texto congelaria todo o aplicativo até o envio do trabalho? O congelamento de aplicativos é um efeito colateral de um bloqueio de aplicativo de thread único durante uma tarefa intensiva da CPU.

Em um aplicativo multithread, as tarefas intensivas da CPU (por exemplo, um trabalho de impressão) podem ser enviadas para um thread de trabalho em segundo plano, liberando o thread da interface do usuário.

Da mesma forma, em um aplicativo com vários processos, o trabalho pode ser enviado via sistema de mensagens (ex IPC, soquetes etc.) para um subprocesso projetado especificamente para processar trabalhos.

Na prática, cada código assíncrono e de processo / multiencadeado tem seus benefícios e desvantagens.

Você pode ver a tendência nas principais plataformas de nuvem, pois elas oferecem instâncias especializadas para processamento vinculado à CPU e instâncias especializadas para processamento vinculado à IO.

Exemplos:

  • O armazenamento (ex Amazon S3, Google Cloud Drive) está vinculado à CPU
  • Servidores da Web são vinculados a IO (Amazon EC2, Google App Engine)
  • Os bancos de dados são ambos, CPU vinculado para gravações / indexação e IO vinculado para leituras

Para colocar em perspectiva ...

Um servidor da web é um exemplo perfeito de uma plataforma fortemente ligada a E / S. Um servidor da web multiencadeado que atribui um encadeamento por conexão não é dimensionado bem porque cada encadeamento incorre em mais sobrecarga devido à quantidade aumentada de alternância de contexto e bloqueio de encadeamento em recursos compartilhados. Enquanto um servidor da Web assíncrono usaria um único espaço de endereço.

Da mesma forma, um aplicativo especializado para codificação de vídeo funcionaria muito melhor em um ambiente multithread porque o processamento pesado envolvido bloqueava o thread principal até que o trabalho fosse concluído. Existem maneiras de mitigar isso, mas é muito mais fácil ter um único encadeamento gerenciando uma fila, um segundo encadeamento gerenciando a limpeza e um conjunto de encadeamentos gerenciando o processamento pesado. A comunicação entre os encadeamentos ocorre apenas quando as tarefas são atribuídas / concluídas, de modo que a sobrecarga de bloqueio de encadeamento é mantida no mínimo.

A melhor aplicação geralmente usa uma combinação de ambos. Um aplicativo da web, por exemplo, pode usar o nginx (por exemplo, async single-threaded) como um balanceador de carga para gerenciar o torrent de solicitações recebidas, um servidor da web assíncrono semelhante (ex Node.js) para manipular solicitações http e um conjunto de servidores multithread lidar com upload / streaming / codificação de conteúdo, etc ...

Ao longo dos anos, houve muitas guerras religiosas entre modelos multithread, multi-process e assíncrono. Como na maioria das coisas, a melhor resposta realmente deve ser "depende".

Segue a mesma linha de pensamento que justifica o uso de arquiteturas de GPU e CPU em paralelo. Dois sistemas especializados em execução em conjunto podem ter uma melhoria muito maior do que uma única abordagem monolítica.

Nem são melhores porque ambos têm seus usos. Use a melhor ferramenta para o trabalho.

Atualizar:

Eu removi a referência ao Apache e fiz uma pequena correção. O Apache usa um modelo de multiprocessos que bifurca um processo para cada solicitação, aumentando a quantidade de alternância de contexto no nível do kernel. Além disso, como a memória não pode ser compartilhada entre os processos, cada solicitação incorre em um custo adicional de memória.

O multithreading evita a necessidade de memória adicional, pois depende de uma memória compartilhada entre os threads. A memória compartilhada remove a sobrecarga adicional da memória, mas ainda incorre na penalidade do aumento da alternância de contexto. Além disso, para garantir que as condições de corrida não aconteçam, são necessários bloqueios de encadeamento (que garantem acesso exclusivo a apenas um encadeamento por vez) para quaisquer recursos compartilhados entre encadeamentos.

É engraçado o que você diz: "os programadores parecem adorar simultaneidade e programas multithread em geral". A programação multithread é universalmente temida por qualquer pessoa que tenha feito uma quantidade substancial disso em seu tempo. Bloqueios inoperantes (um bug que ocorre quando um recurso é bloqueado por duas fontes diferentes, impedindo ambos de terminar sempre) e condições de corrida (nas quais o programa gera erroneamente o resultado errado aleatoriamente devido ao sequenciamento incorreto) são algumas das mais difíceis de rastrear para baixo e consertar.

Update2:

Contrariamente à afirmação geral de que o IPC é mais rápido que as comunicações em rede (por exemplo, soquete). Nem sempre é esse o caso . Lembre-se de que essas são generalizações e detalhes específicos da implementação podem ter um enorme impacto no resultado.


por que um programador deve executar vários processos? Quero dizer, suponho que, com mais de um processo, você também precise de algum tipo de comunicação entre processos que possa adicionar uma sobrecarga significativa. Isso é algo como a maneira antiga de programadores de janelas de fazer as coisas? quando devo ir multiprocesso? Obrigado pela sua resposta, a propósito, uma boa imagem do que são assíncronas e multithread.
usar o seguinte comando

1
Você está assumindo que a comunicação entre processos aumentaria a sobrecarga geral. No entanto, se o estado de processamento for imutável, ou apenas precisar lidar com a sincronização após o início / conclusão. pode ser muito mais eficiente se espalhar para tarefas mais paralelas. O padrão de ator é um bom exemplo, e se você ainda não leu, vale a pena ler. akka.io
sylvanaar

1
@ user1849534 Vários threads podem se comunicar via memória compartilhada + bloqueio ou IPC. O bloqueio é mais fácil, mas mais difícil de depurar, se você cometer um erro (por exemplo, perdeu um bloqueio, bloqueio morto). O IPC é melhor se você tiver muitos encadeamentos de trabalho, pois o bloqueio não é bem dimensionado. De qualquer forma, se você estiver usando uma abordagem multithread, é importante manter a comunicação / sincronização entre threads no mínimo absoluto (isto é, minimizar a sobrecarga).
Evan Solha

1
@ akka.io Você está completamente certo. A imutabilidade é uma maneira de minimizar / eliminar a sobrecarga do bloqueio, mas você ainda incorre no custo de tempo da alternância de contexto. Se você deseja estender a resposta para incluir os detalhes sobre como a imutabilidade pode resolver problemas de sincronização de threads, fique à vontade. O ponto principal que eu pretendia ilustrar é que há casos em que a comunicação assíncrona tem uma vantagem distinta sobre o processo / multiencadeado e vice-versa.
Evan Plaice

(cont.) Mas, honestamente, se eu precisasse de muita capacidade de processamento vinculado à CPU, pularia o modelo de ator e o construía para ser capaz de escalar para vários nós da rede. A melhor solução que eu vi para isso é usar o modelo de ventilador de tarefas do 0MQ em comunicações no nível do soquete. Veja a Figura 5 @ zguide.zeromq.org/page:all .
Evan Plaice

13

A abordagem assíncrona da Microsoft é um bom substituto para os objetivos mais comuns da programação multithread: aprimorando a capacidade de resposta com relação às tarefas de E / S.

No entanto, é importante perceber que a abordagem assíncrona não é capaz de melhorar o desempenho nem melhorar a capacidade de resposta em relação às tarefas intensivas da CPU.

Multithreading para capacidade de resposta

Multithreading para capacidade de resposta é a maneira tradicional de manter um programa responsivo durante tarefas pesadas de E / S ou tarefas pesadas de computação. Você salva os arquivos em um encadeamento em segundo plano, para que o usuário possa continuar seu trabalho, sem precisar esperar pelo disco rígido para concluir sua tarefa. O encadeamento IO geralmente bloqueia a espera de que parte de uma gravação seja concluída, portanto, as alternâncias de contexto são frequentes.

Da mesma forma, ao executar um cálculo complexo, você deseja permitir a alternância regular de contexto para que a interface do usuário possa permanecer responsiva e o usuário não acha que o programa travou.

O objetivo aqui não é, em geral, fazer com que vários threads sejam executados em diferentes CPUs. Em vez disso, estamos apenas interessados ​​em fazer com que as alternâncias de contexto ocorram entre a tarefa em segundo plano de longa execução e a interface do usuário, para que ela possa atualizar e responder ao usuário enquanto a tarefa em segundo plano estiver em execução. Em geral, a interface do usuário não consome muita energia da CPU, e a estrutura de segmentação ou o SO geralmente decide executá-los na mesma CPU.

Na verdade, perdemos o desempenho geral devido ao custo extra da alternância de contexto, mas não nos importamos porque o desempenho da CPU não era nosso objetivo. Sabemos que geralmente temos mais energia da CPU do que precisamos e, portanto, nosso objetivo em relação ao multithreading é executar uma tarefa para o usuário sem perder tempo.

A alternativa "assíncrona"

A "abordagem assíncrona" altera essa imagem, habilitando as alternâncias de contexto em um único thread. Isso garante que todas as nossas tarefas sejam executadas em uma única CPU e pode fornecer algumas melhorias modestas de desempenho em termos de menos criação / limpeza de encadeamentos e menos alternâncias reais de contexto entre encadeamentos.

Em vez de criar um novo encadeamento para aguardar o recebimento de um recurso de rede (por exemplo, fazer o download de uma imagem), asyncé usado um método, que awaité a imagem que fica disponível e, enquanto isso, gera o método de chamada.

A principal vantagem aqui é que você não precisa se preocupar com problemas de encadeamento, como evitar conflitos, pois não está usando bloqueios e sincronização, e há um pouco menos de trabalho para o programador configurar o encadeamento em segundo plano e voltar no segmento da interface do usuário quando o resultado voltar para atualizar a interface do usuário com segurança.

Eu não examinei muito profundamente os detalhes técnicos, mas minha impressão é que gerenciar o download com uma atividade leve ocasional da CPU se torna uma tarefa não para um encadeamento separado, mas algo mais como uma tarefa na fila de eventos da interface do usuário e quando o o download é concluído, o método assíncrono é retomado a partir dessa fila de eventos. Em outras palavras, awaitsignifica algo semelhante a "verificar se o resultado que eu preciso está disponível, se não, me colocar de volta na fila de tarefas deste encadeamento".

Observe que essa abordagem não resolveria o problema de uma tarefa intensiva de CPU: não há dados a aguardar; portanto, não podemos obter as alternâncias de contexto que precisamos que ocorram sem criar um encadeamento de trabalho em segundo plano real. Obviamente, ainda pode ser conveniente usar um método assíncrono para iniciar o encadeamento em segundo plano e retornar o resultado, em um programa que utiliza amplamente a abordagem assíncrona.

Multithreading for Performance

Como você fala sobre "desempenho", também gostaria de discutir como o multithreading pode ser usado para obter ganhos de desempenho, algo que é totalmente impossível com a abordagem assíncrona de thread único.

Quando você está realmente em uma situação em que não possui energia suficiente da CPU em uma única CPU e deseja usar o multithreading para obter desempenho, na maioria das vezes é difícil. Por outro lado, se uma CPU não tem capacidade de processamento suficiente, também é frequentemente a única solução que pode permitir que seu programa faça o que você gostaria de realizar em um prazo razoável, que é o que faz o trabalho valer a pena.

Paralelismo trivial

Obviamente, às vezes pode ser fácil obter uma aceleração real do multithreading.

Se você tiver um grande número de tarefas independentes de uso intensivo de computação (ou seja, tarefas cujos dados de entrada e saída são muito pequenos em relação aos cálculos que devem ser executados para determinar o resultado), poderá obter uma aceleração significativa com criando um conjunto de encadeamentos (dimensionados adequadamente com base no número de CPUs disponíveis) e fazendo com que um encadeamento mestre distribua o trabalho e colete os resultados.

Multithreading prático para desempenho

Eu não quero me apresentar muito como especialista, mas minha impressão é que, em geral, o multithreading mais prático para o desempenho que acontece hoje em dia é procurar lugares em um aplicativo que tenha paralelismo trivial e usar vários threads colher os benefícios.

Como em qualquer otimização, geralmente é melhor otimizar depois que você cria o perfil do desempenho do programa e identifica os pontos críticos: é fácil desacelerar um programa, decidindo arbitrariamente que essa parte deve ser executada em um segmento e a parte em outro, sem primeiro determinando se as duas partes estão ocupando uma parte significativa do tempo da CPU.

Um encadeamento extra significa mais custos de configuração / desmontagem e mais comutadores de contexto ou mais custos de comunicação entre CPU. Se não estiver fazendo o trabalho suficiente para compensar esses custos em uma CPU separada e não precisar ser um encadeamento separado por motivos de capacidade de resposta, isso atrasará as coisas sem nenhum benefício.

Procure tarefas com poucas interdependências e que estejam ocupando uma parte significativa do tempo de execução do seu programa.

Se eles não tiverem interdependências, é um caso de paralelismo trivial, você pode facilmente configurar cada um com um thread e aproveitar os benefícios.

Se você puder encontrar tarefas com interdependência limitada, para que o bloqueio e a sincronização para troca de informações não as reduzam significativamente, o multithreading pode acelerar o processo, desde que você tome cuidado para evitar os perigos do impasse devido à lógica defeituosa ao sincronizar ou resultados incorretos devido à não sincronização quando necessário.

Como alternativa, alguns dos aplicativos mais comuns para multithreading não procuram (de certo modo) a aceleração de um algoritmo predeterminado, mas sim um orçamento maior para o algoritmo que eles planejam escrever: se você estiver escrevendo um mecanismo de jogo , e sua IA precisa tomar uma decisão dentro da sua taxa de quadros, geralmente você pode dar à sua AI um orçamento maior de ciclo de CPU, se pode fornecer sua própria CPU.

No entanto, certifique-se de criar um perfil dos threads e garantir que eles estejam fazendo o trabalho suficiente para compensar o custo em algum momento.

Algoritmos Paralelos

Também existem muitos problemas que podem ser acelerados usando vários processadores, mas que são monolíticos demais para serem simplesmente divididos entre as CPUs.

Os algoritmos paralelos devem ser cuidadosamente analisados ​​em relação aos seus tempos de execução grandes com relação ao melhor algoritmo não paralelo disponível, pois é muito fácil para o custo de comunicação entre CPUs eliminar todos os benefícios do uso de várias CPUs. Em geral, eles devem usar menos comunicação entre CPU (em termos de grande O) do que usar cálculos em cada CPU.

No momento, ainda é amplamente um espaço para pesquisa acadêmica, em parte por causa da análise complexa necessária, em parte porque o paralelismo trivial é bastante comum, em parte porque ainda não temos tantos núcleos de CPU em nossos computadores que problemas que não pode ser resolvido em um período de tempo razoável em uma CPU pode ser resolvido em um período de tempo razoável usando todas as nossas CPUs.


+1 para uma resposta obviamente bem pensada. Eu recomendaria cautela ao aceitar as sugestões da Microsoft pelo valor nominal. Lembre-se de que o .NET é uma plataforma síncrona em primeiro lugar; portanto, o ecossistema é tendencioso para fornecer melhores instalações / documentação que suportam a criação de soluções síncronas. O oposto seria verdadeiro para plataformas assíncronas como Node.js.
quer

3

o aplicativo não responde e é apenas lento e desagradável.

E aí está o seu problema. Uma interface de usuário responsiva não cria um aplicativo com desempenho. Muitas vezes o oposto. É gasto muito tempo verificando a entrada da interface do usuário em vez de fazer com que os threads de trabalho façam seu trabalho.

Tanto quanto 'apenas' ter uma abordagem assíncrona, isso também é multithreading, embora tenha sido aprimorado para esse caso de uso específico na maioria dos ambientes . Em outros, esse assíncrono é feito através de corotinas que nem sempre são concorrentes.

Francamente, acho as operações assíncronas mais difíceis de raciocinar e usar de uma maneira que realmente oferece benefícios (desempenho, robustez, facilidade de manutenção), mesmo em comparação com ... abordagens mais manuais.


porque ? por exemplo, o que você acha tão bananas na biblioteca de sinais de impulso2?
user1849534
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.