Eu pensei que o objetivo de um computador com vários núcleos é que ele pode executar vários threads simultaneamente. Nesse caso, se você possui uma máquina com quatro núcleos, qual o sentido de ter mais de 4 threads em execução por vez? Eles não estariam apenas roubando tempo (CPU Resources) um do outro?
multithreading
hardware
cpu-cores
Nick Heiner
fonte
fonte
Respostas:
A resposta gira em torno do objetivo dos encadeamentos, que é o paralelismo: executar várias linhas de execução separadas ao mesmo tempo. Em um sistema 'ideal', você teria um thread em execução por núcleo: sem interrupção. Na realidade, este não é o caso. Mesmo se você tiver quatro núcleos e quatro threads de trabalho, o processo e os threads serão constantemente trocados por outros processos e threads. Se você estiver executando um sistema operacional moderno, todo processo possui pelo menos um thread e muitos têm mais. Todos esses processos estão em execução ao mesmo tempo. Você provavelmente tem várias centenas de threads em execução na sua máquina agora. Você nunca terá uma situação em que um thread é executado sem ter o tempo 'roubado' dele. (Bem, você pode se estiver executando em tempo real, se você estiver usando um sistema operacional em tempo real ou, mesmo no Windows, use uma prioridade de encadeamento em tempo real. Mas é raro.)
Com isso como pano de fundo, a resposta: Sim, mais de quatro threads em uma verdadeira máquina de quatro núcleos podem fornecer uma situação em que eles "roubam tempo um do outro", mas apenas se cada thread individual precisar de 100% da CPU . Se um encadeamento não estiver funcionando 100% (como um encadeamento da interface do usuário pode não estar, ou um encadeamento fazendo uma pequena quantidade de trabalho ou aguardando outra coisa), outro encadeamento agendado é realmente uma boa situação.
Na verdade, é mais complicado do que isso:
E se você tiver cinco partes do trabalho que precisam ser feitas de uma só vez? Faz mais sentido executá-los todos de uma vez, do que executar quatro deles e depois executar o quinto depois.
É raro um segmento realmente precisar de 100% da CPU. No momento em que usa E / S de disco ou de rede, por exemplo, pode ser possível que você gaste algum tempo esperando sem fazer nada útil. Esta é uma situação muito comum.
Se você tem um trabalho que precisa ser executado, um mecanismo comum é usar um pool de threads. Parece fazer sentido ter o mesmo número de threads que os núcleos, mas o pool de threads .Net tem até 250 threads disponíveis por processador . Não sei por que eles fazem isso, mas meu palpite é o tamanho das tarefas que são dadas para serem executadas nos threads.
Portanto, roubar tempo não é uma coisa ruim (e também não é um roubo: é como o sistema deve funcionar.) Escreva seus programas multithread com base no tipo de trabalho que os threads farão, o que pode não ser a CPU -limite. Descubra o número de threads necessários com base em perfis e medidas. Você pode achar mais útil pensar em termos de tarefas ou trabalhos, em vez de threads: escreva objetos de trabalho e entregue-os a um pool para execução. Finalmente, a menos que seu programa seja realmente crítico para o desempenho, não se preocupe muito :)
fonte
Só porque um segmento existe nem sempre significa que ele está sendo executado ativamente. Muitas aplicações de encadeamentos envolvem alguns dos encadeamentos que vão dormir até que seja hora de fazer alguma coisa - por exemplo, entrada do usuário acionando encadeamentos para ativar, executar algum processamento e voltar a dormir.
Essencialmente, os encadeamentos são tarefas individuais que podem operar independentemente um do outro, sem a necessidade de estar ciente do progresso de outra tarefa. É bem possível ter mais desses recursos do que você pode executar simultaneamente; eles ainda são úteis por conveniência, mesmo que às vezes tenham que esperar na fila atrás um do outro.
fonte
O ponto é que, apesar de não haver nenhuma aceleração real quando a contagem de threads excede a contagem de núcleos, é possível usar segmentos para desemaranhar partes da lógica que não deveriam ser interdependentes.
Mesmo em um aplicativo moderadamente complexo, o uso de um único encadeamento tenta fazer tudo rapidamente, cria um hash do 'fluxo' do seu código. O thread único passa a maior parte do tempo pesquisando isso, verificando isso, chamando condicionalmente as rotinas conforme necessário, e fica difícil ver algo além de um pântano de minúcias.
Compare isso com o caso em que você pode dedicar threads a tarefas para que, olhando para qualquer thread individual, possa ver o que esse thread está fazendo. Por exemplo, um encadeamento pode bloquear a espera na entrada de um soquete, analisar o fluxo em mensagens, filtrar mensagens e, quando uma mensagem válida aparecer, transmiti-la para outro encadeamento de trabalho. O segmento de trabalho pode trabalhar com entradas de várias outras fontes. O código para cada um deles exibirá um fluxo limpo e intencional, sem a necessidade de fazer verificações explícitas de que não há mais nada a fazer.
Particionar o trabalho dessa maneira permite que seu aplicativo conte com o sistema operacional para agendar o que fazer em seguida com a CPU, para que você não precise fazer verificações condicionais explícitas em todo o aplicativo sobre o que pode bloquear e o que está pronto para processar.
fonte
Se um encadeamento estiver aguardando um recurso (como carregar um valor da RAM em um registro, E / S de disco, acesso à rede, iniciar um novo processo, consultar um banco de dados ou aguardar a entrada do usuário), o processador poderá trabalhar em um thread diferente e retorne ao primeiro thread assim que o recurso estiver disponível. Isso reduz o tempo que a CPU passa ociosa, pois ela pode executar milhões de operações em vez de ficar ociosa.
Considere um segmento que precisa ler dados de um disco rígido. Em 2014, um núcleo típico de processador opera a 2,5 GHz e pode executar 4 instruções por ciclo. Com um tempo de ciclo de 0,4 ns, o processador pode executar 10 instruções por nanossegundo. Com os tempos de busca típicos do disco rígido mecânico em torno de 10 milissegundos, o processador é capaz de executar 100 milhões de instruções no tempo necessário para ler um valor do disco rígido. Pode haver melhorias significativas no desempenho de discos rígidos com um pequeno cache (buffer de 4 MB) e unidades híbridas com alguns GB de armazenamento, pois a latência de dados para leituras sequenciais ou leituras da seção híbrida pode ser várias ordens de magnitude mais rapidamente.
Um núcleo de processador pode alternar entre threads (o custo para pausar e retomar uma thread é de cerca de 100 ciclos de clock) enquanto o primeiro thread aguarda uma entrada de alta latência (algo mais caro que registradores (1 clock) e RAM (5 nanossegundos)). E / S de disco, acesso à rede (latência de 250ms), leitura de dados de um CD ou barramento lento ou chamada de banco de dados. Ter mais threads do que núcleos significa que um trabalho útil pode ser feito enquanto as tarefas de alta latência são resolvidas.
A CPU possui um agendador de encadeamentos que atribui prioridade a cada encadeamento e permite que um encadeamento entre em suspensão e, em seguida, retoma após um tempo predeterminado. O trabalho do agendador de encadeamentos é reduzir o thrashing, o que ocorreria se cada encadeamento executasse apenas 100 instruções antes de voltar a dormir. A sobrecarga dos threads de comutação reduziria a taxa de transferência útil total do núcleo do processador.
Por esse motivo, convém dividir seu problema em um número razoável de threads. Se você estivesse escrevendo código para executar a multiplicação da matriz, a criação de um encadeamento por célula na matriz de saída pode ser excessiva, enquanto um encadeamento por linha ou por n linhas na matriz de saída pode reduzir o custo adicional de criação, pausa e retomada de encadeamentos.
É também por isso que a previsão de ramificação é importante. Se você tiver uma instrução if que exija o carregamento de um valor da RAM, mas o corpo das instruções if e else usar valores já carregados nos registradores, o processador poderá executar uma ou ambas as ramificações antes que a condição seja avaliada. Quando a condição retornar, o processador aplicará o resultado da ramificação correspondente e descartará a outra. A execução de um trabalho potencialmente inútil aqui é provavelmente melhor do que mudar para um thread diferente, o que pode levar a problemas.
À medida que nos mudamos dos processadores single-core de alta velocidade para os processadores multi-core, o design do chip concentrou-se em compactar mais núcleos por matriz, melhorando o compartilhamento de recursos no chip entre núcleos, melhores algoritmos de previsão de ramificação, melhor sobrecarga de troca de threads, e melhor agendamento de threads.
fonte
A maioria das respostas acima fala sobre desempenho e operação simultânea. Vou abordar isso de um ângulo diferente.
Vamos considerar o caso de, digamos, um programa simplificado de emulação de terminal. Você precisa fazer o seguinte:
(Os emuladores de terminal reais fazem mais, inclusive potencialmente ecoando as coisas que você digita no visor, mas passaremos por cima disso por enquanto.)
Agora, o loop para leitura do controle remoto é simples, conforme o pseudocódigo a seguir:
O loop para monitorar o teclado e enviar também é simples:
O problema, porém, é que você precisa fazer isso simultaneamente. O código agora deve se parecer mais com isso se você não tiver o threading:
A lógica, mesmo neste exemplo deliberadamente simplificado que não leva em conta a complexidade das comunicações no mundo real, é bastante ofuscada. No entanto, com o encadeamento, mesmo em um único núcleo, os dois loops de pseudocódigo podem existir independentemente, sem entrelaçar sua lógica. Como os dois encadeamentos serão basicamente vinculados à E / S, eles não sobrecarregam a CPU, mesmo sendo estritamente mais desperdiçados em recursos da CPU do que o loop integrado.
Agora, é claro, o uso no mundo real é mais complicado que o acima. Mas a complexidade do loop integrado aumenta exponencialmente à medida que você adiciona mais preocupações ao aplicativo. A lógica fica cada vez mais fragmentada e você precisa começar a usar técnicas como máquinas de estado, corotinas etc. para obter coisas gerenciáveis. Gerenciável, mas não legível. A segmentação mantém o código mais legível.
Então, por que você não usaria rosqueamento?
Bem, se suas tarefas são ligadas à CPU em vez de E / S, o encadeamento realmente torna o sistema lento. O desempenho sofrerá. Muito, em muitos casos. ("Thrashing" é um problema comum se você eliminar muitos threads vinculados à CPU. Você acaba gastando mais tempo alterando os threads ativos do que executando o conteúdo dos próprios threads.) Além disso, um dos motivos pelos quais a lógica acima é tão simples é que escolhi deliberadamente um exemplo simplista (e irrealista). Se você quiser repetir o que foi digitado na tela, terá um novo mundo de mágoas ao introduzir o bloqueio de recursos compartilhados. Com apenas um recurso compartilhado, isso não é um problema, mas começa a se tornar um problema cada vez maior, à medida que você tem mais recursos para compartilhar.
Então, no final, enfiar é sobre muitas coisas. Por exemplo, trata-se de tornar os processos vinculados à E / S mais responsivos (mesmo que menos eficientes no geral), como alguns já disseram. É também tornar a lógica mais fácil de seguir (mas apenas se você minimizar o estado compartilhado). Trata-se de muitas coisas, e você precisa decidir se suas vantagens superam suas desvantagens caso a caso.
fonte
Embora você certamente possa usar threads para acelerar os cálculos, dependendo do seu hardware, um dos principais usos deles é fazer mais de uma coisa por vez, por motivos de facilidade de uso.
Por exemplo, se você precisar fazer algum processamento em segundo plano e também permanecer responsivo à entrada da interface do usuário, poderá usar threads. Sem threads, a interface do usuário seria interrompida toda vez que você tentava executar qualquer processamento pesado.
Consulte também esta pergunta relacionada: Usos práticos para threads
fonte
Discordo totalmente da afirmação de @ kyoryu de que o número ideal é um thread por CPU.
Pense da seguinte maneira: por que temos sistemas operacionais de multiprocessamento? Na maior parte da história do computador, quase todos os computadores tinham uma CPU. No entanto, a partir da década de 1960, todos os computadores "reais" tinham sistemas operacionais de multiprocessamento (também conhecido como multitarefa).
Você executa vários programas para que um possa ser executado enquanto outros são bloqueados para coisas como E / S.
deixa de lado argumentos sobre se as versões do Windows anteriores ao NT eram multitarefas. Desde então, todo sistema operacional real tinha multitarefa. Alguns não o expõem aos usuários, mas estão lá de qualquer maneira, fazendo coisas como ouvir rádio no celular, conversar com o chip GPS, aceitar entrada do mouse etc.
Threads são apenas tarefas um pouco mais eficientes. Não há diferença fundamental entre uma tarefa, processo e thread.
Uma CPU é uma coisa terrível a ser desperdiçada; portanto, tenha muitas coisas prontas para usá-la quando puder.
Concordo que, com a maioria das linguagens procedurais, C, C ++, Java etc, escrever um código de thread adequado é muito trabalhoso. Com 6 CPUs principais no mercado hoje e 16 CPUs não muito distantes, espero que as pessoas se afastem desses idiomas antigos, pois o multi-threading é cada vez mais um requisito crítico.
Discordância com @kyoryu é apenas IMHO, o resto é fato.
fonte
Imagine um servidor da Web que precise atender a um número arbitrário de solicitações. Você precisa atender as solicitações em paralelo, pois, caso contrário, cada nova solicitação deverá aguardar até que todas as outras solicitações sejam concluídas (incluindo o envio da resposta pela Internet). Nesse caso, a maioria dos servidores da Web possui muito menos núcleos do que o número de solicitações que eles geralmente atendem.
Isso também facilita para o desenvolvedor do servidor: você só precisa escrever um programa de encadeamento que atenda a uma solicitação, não precisa armazenar várias solicitações, a ordem em que as atende e assim por diante.
fonte
Muitos threads permanecerão em espera, aguardando a entrada do usuário, E / S e outros eventos.
fonte
Os threads podem ajudar na capacidade de resposta em aplicativos de interface do usuário. Além disso, você pode usar threads para obter mais trabalho de seus núcleos. Por exemplo, em um único núcleo, você pode ter um thread fazendo IO e outro fazendo algum cálculo. Se tivesse um encadeamento único, o núcleo poderia estar essencialmente ocioso aguardando a conclusão do IO. Esse é um exemplo de nível bastante alto, mas os threads podem definitivamente ser usados para sobrecarregar sua CPU um pouco mais.
fonte
Um processador, ou CPU, é o chip físico conectado ao sistema. Um processador pode ter vários núcleos (um núcleo é a parte do chip capaz de executar instruções). Um núcleo pode aparecer no sistema operacional como múltiplos processadores virtuais se for capaz de executar simultaneamente vários threads (um thread é uma única sequência de instruções).
Um processo é outro nome para um aplicativo. Geralmente, os processos são independentes um do outro. Se um processo morre, ele não causa a morte de outro processo. É possível que os processos se comuniquem ou compartilhem recursos como memória ou E / S.
Cada processo possui um espaço de endereço e uma pilha separados. Um processo pode conter vários threads, cada um capaz de executar instruções simultaneamente. Todos os threads em um processo compartilham o mesmo espaço de endereço, mas cada thread terá sua própria pilha.
Esperamos que com essas definições e pesquisas adicionais usando esses fundamentos ajude sua compreensão.
fonte
O uso ideal de threads é, de fato, um por núcleo.
No entanto, a menos que você use exclusivamente E / S assíncronas / sem bloqueio, há uma boa chance de que você tenha segmentos bloqueados no E / S em algum momento, o que não utilizará sua CPU.
Além disso, linguagens de programação típicas dificultam um pouco o uso de 1 thread por CPU. Os idiomas projetados para a simultaneidade (como o Erlang) podem facilitar o uso de threads extras.
fonte
Da maneira como algumas APIs são projetadas, você não tem escolha a não ser executá-las em um encadeamento separado (qualquer coisa com operações de bloqueio). Um exemplo seria as bibliotecas HTTP do Python (AFAIK).
Normalmente, isso não é um grande problema (se houver, o SO ou a API deverá ser fornecido com um modo operacional assíncrono alternativo, ou seja:
select(2)
:), porque provavelmente significa que o encadeamento ficará inativo durante a espera de I / O conclusão. Por outro lado, se algo está fazendo um cálculo pesado, você tem que colocá-lo em um segmento separado do que dizer, o segmento de GUI (a menos que você gosta de multiplexação manual).fonte
Sei que essa é uma pergunta super antiga, com muitas boas respostas, mas estou aqui para apontar algo que é importante no ambiente atual:
Se você deseja criar um aplicativo para multiencadeamento, não deve projetar para uma configuração de hardware específica. A tecnologia da CPU vem avançando rapidamente há anos e as contagens principais estão aumentando constantemente. Se você projetar deliberadamente seu aplicativo de forma que ele use apenas 4 threads, estará potencialmente se restringindo a um sistema octa-core (por exemplo). Agora, até sistemas de 20 núcleos estão disponíveis comercialmente, portanto esse projeto definitivamente está fazendo mais mal do que bem.
fonte
Em resposta à sua primeira conjectura: máquinas com vários núcleos podem executar simultaneamente vários processos, não apenas os múltiplos threads de um único processo.
Em resposta à sua primeira pergunta: o objetivo de vários encadeamentos geralmente é executar simultaneamente várias tarefas em um aplicativo. Os exemplos clássicos na rede são um programa de e-mail enviando e recebendo e-mail e um servidor da web recebendo e enviando solicitações de páginas. (Observe que é essencialmente impossível reduzir um sistema como o Windows para executar apenas um thread ou mesmo apenas um processo. Execute o Gerenciador de tarefas do Windows e você verá uma longa lista de processos ativos, muitos dos quais executando vários threads. )
Em resposta à sua segunda pergunta: a maioria dos processos / encadeamentos não é vinculada à CPU (ou seja, não está sendo executada continuamente e sem interrupção), mas, em vez disso, pare e aguarde com frequência a conclusão da E / S. Durante essa espera, outros processos / threads podem ser executados sem "roubar" o código em espera (mesmo em uma máquina de núcleo único).
fonte
Um encadeamento é uma abstração que permite que você escreva um código tão simples quanto uma sequência de operação, sem saber que o código é executado entrelaçado com outro código, ou estacionado esperando por E / S ou (talvez um pouco mais atento) à espera de outro encadeamento eventos ou mensagens.
fonte
O ponto é que a grande maioria dos programadores não entende como projetar uma máquina de estado. Ser capaz de colocar tudo em seu próprio encadeamento libera o programador de ter que pensar em como representar com eficiência o estado de diferentes cálculos em andamento, para que possam ser interrompidos e depois retomados.
Como exemplo, considere a compactação de vídeo, uma tarefa que exige muita CPU. Se você estiver usando uma ferramenta GUI, provavelmente desejará que a interface permaneça responsiva (mostre progresso, responda a solicitações de cancelamento, redimensionamento de janelas etc.). Portanto, você projeta seu software de codificador para processar uma unidade grande (um ou mais quadros) de cada vez e executá-la em seu próprio encadeamento, separado da interface do usuário.
É claro que, uma vez que você perceba que seria bom poder salvar o estado de codificação em andamento para poder fechar o programa para reiniciar ou jogar um jogo que requer muitos recursos, você deve ter aprendido como projetar máquinas de estado a partir do começando. Ou você decide criar um novo problema de hibernação de processos no sistema operacional para suspender e retomar aplicativos individuais em disco ...
fonte