Embora os threads possam acelerar a execução do código, eles são realmente necessários? Cada pedaço de código pode ser feito usando um único thread ou existe algo que só pode ser realizado usando vários threads?
Embora os threads possam acelerar a execução do código, eles são realmente necessários? Cada pedaço de código pode ser feito usando um único thread ou existe algo que só pode ser realizado usando vários threads?
Respostas:
Primeiro de tudo, os threads não podem acelerar a execução do código. Eles não fazem o computador funcionar mais rápido. Tudo o que eles podem fazer é aumentar a eficiência do computador usando o tempo que seria desperdiçado. Em certos tipos de processamento, essa otimização pode aumentar a eficiência e diminuir o tempo de execução.
A resposta simples é sim. Você pode escrever qualquer código para ser executado em um único encadeamento. Prova: Um sistema de processador único pode executar instruções apenas linearmente. Ter várias linhas de execução é feita pelas interrupções do processamento do sistema operacional, salvando o estado do encadeamento atual e iniciando outro.
A resposta complexa é ... mais complexa! A razão pela qual os programas multithread podem ser mais eficientes que os lineares é devido a um "problema" de hardware. A CPU pode executar cálculos mais rapidamente do que IO de memória e armazenamento físico. Portanto, uma instrução "add", por exemplo, executa muito mais rapidamente do que uma "busca". Caches e busca de instruções de programas dedicados (não tenho certeza do termo exato aqui) podem combater isso até certo ponto, mas o problema da velocidade permanece.
O encadeamento é uma maneira de combater essa incompatibilidade usando a CPU para obter instruções vinculadas à CPU enquanto as instruções de E / S estão sendo concluídas. Um plano de execução de encadeamento típico provavelmente seria: Buscar dados, processar dados, gravar dados. Suponha que a busca e a gravação levem três ciclos e o processamento leve um, para fins ilustrativos. Você pode ver que, enquanto o computador está lendo ou escrevendo, ele não está fazendo nada por 2 ciclos cada? Claramente, está sendo preguiçoso e precisamos quebrar nosso chicote de otimização!
Podemos reescrever o processo usando o threading para usar esse tempo perdido:
E assim por diante. Obviamente, este é um exemplo um tanto artificial, mas você pode ver como essa técnica pode utilizar o tempo que seria gasto esperando pela E / S.
Observe que o encadeamento, como mostrado acima, só pode aumentar a eficiência em processos fortemente vinculados a E / S. Se um programa estiver calculando principalmente as coisas, não haverá muitos "buracos" nos quais poderíamos trabalhar mais. Além disso, há uma sobrecarga de várias instruções ao alternar entre threads. Se você executar muitos threads, a CPU passará a maior parte do tempo trocando e não trabalhando muito no problema. Isso é chamado de thrashing .
Tudo está bem para um processador de núcleo único, mas a maioria dos processadores modernos tem dois ou mais núcleos. Os threads ainda têm o mesmo objetivo - maximizar o uso da CPU, mas desta vez temos a capacidade de executar duas instruções separadas ao mesmo tempo. Isso pode diminuir o tempo de execução por um fator, no entanto, muitos núcleos estão disponíveis, porque o computador é realmente multitarefa, não alternando o contexto.
Com vários núcleos, os threads fornecem um método de dividir o trabalho entre os dois núcleos. O acima ainda se aplica a cada núcleo individual; Um programa que execute uma eficiência máxima com dois threads em um núcleo provavelmente executará com eficiência máxima com cerca de quatro threads em dois núcleos. (A eficiência é medida aqui por execuções mínimas de instruções NOP.)
Os problemas com a execução de threads em vários núcleos (em oposição a um único núcleo) geralmente são resolvidos pelo hardware. A CPU garantirá que bloqueie os locais de memória apropriados antes de ler / gravar nela. (Eu li que ele usa um bit de sinalizador especial na memória para isso, mas isso pode ser realizado de várias maneiras.) Como programador com linguagens de nível superior, você não precisa se preocupar com mais nada em dois núcleos. teria que ser com um.
TL; DR: os threads podem dividir o trabalho para permitir que o computador processe várias tarefas de forma assíncrona. Isso permite que o computador funcione com a máxima eficiência utilizando todo o tempo de processamento disponível, em vez de travar quando um processo está aguardando por um recurso.
O que vários threads podem fazer que um único thread não pode?
Nada.
Esboço simples de prova:
Observe, no entanto, que há uma grande suposição oculta: a linguagem usada no thread único é Turing-complete.
Portanto, a pergunta mais interessante seria: "A adição de apenas multi-threading a um idioma não completo de Turing pode torná-lo completo?" E eu acredito que a resposta é "Sim".
Vamos usar os idiomas funcionais totais. [Para aqueles que não estão familiarizados: assim como a Programação Funcional está programando com funções, a Programação Funcional Total está programando com o total de funções.]
As linguagens funcionais totais obviamente não são completas para Turing: você não pode escrever um loop infinito em uma TFPL (na verdade, essa é praticamente a definição de "total"), mas você pode em uma máquina de Turing; portanto, existe pelo menos um programa que não pode ser gravado em um TFPL, mas em um UTM; portanto, os TFPLs são menos poderosos em termos computacionais que os UTMs.
No entanto, assim que você adiciona o encadeamento a um TFPL, obtém loops infinitos: basta fazer cada iteração do loop em um novo encadeamento. Cada thread individual sempre retorna um resultado, portanto é Total, mas cada thread também gera um novo thread que executa a próxima iteração, ad infinitum.
Eu acho que essa linguagem seria completa em Turing.
No mínimo, responde à pergunta original:
O que vários threads podem fazer que um único thread não pode?
Se você tem uma linguagem que não pode fazer loops infinitos, em seguida, multi-threading permite que você faça loops infinitos.
Observe, é claro, que gerar um encadeamento é um efeito colateral e, portanto, nossa linguagem estendida não é mais não apenas total, como também não é mais funcional.
Em teoria, tudo o que um programa multithread faz pode ser feito com um programa de thread único, apenas mais devagar.
Na prática, a diferença de velocidade pode ser tão grande que não há como usar um programa de thread único para a tarefa. Por exemplo, se você tem um trabalho de processamento de dados em lote executando todas as noites e leva mais de 24 horas para concluir um único encadeamento, você não tem outra opção senão torná-lo multithread. (Na prática, o limite provavelmente é ainda menor: geralmente essas tarefas de atualização devem terminar até de manhã cedo, antes que os usuários comecem a usar o sistema novamente. Além disso, outras tarefas podem depender delas, que também devem ser concluídas durante a mesma noite. o tempo de execução disponível pode ser tão baixo quanto algumas horas / minutos.)
Fazer o trabalho de computação em vários threads é uma forma de processamento distribuído; você está distribuindo o trabalho por vários segmentos. Outro exemplo de processamento distribuído (usando vários computadores em vez de vários threads) é o protetor de tela SETI: processar muitos dados de medição em um único processador levaria muito tempo e os pesquisadores prefeririam ver os resultados antes da aposentadoria ;-) No entanto, eles não tem orçamento para alugar um supercomputador por tanto tempo; assim, eles distribuem o trabalho por milhões de PCs domésticos, para torná-lo mais barato.
Embora os encadeamentos pareçam ser um pequeno passo em relação à computação sequencial, na verdade, eles representam um grande passo. Eles descartam as propriedades mais essenciais e atraentes da computação seqüencial: compreensibilidade, previsibilidade e determinismo. Os threads, como modelo de computação, são extremamente não-determinísticos, e o trabalho do programador passa a podar esse não-determinismo.
- O problema com os threads (www.eecs.berkeley.edu/Pubs/TechRpts/2006/EECS-2006-1.pdf).
Embora haja algumas vantagens de desempenho que podem ser obtidas com o uso de encadeamentos em que você pode distribuir o trabalho em vários núcleos, eles geralmente têm um ótimo preço.
Uma das desvantagens do uso de encadeamentos ainda não mencionados aqui é a perda de compartimentação de recursos obtida com espaços de processo encadeados únicos. Por exemplo, digamos que você encontre o caso de um segfault. Em alguns casos, é possível recuperar isso em um aplicativo com vários processos, pois você simplesmente deixa a criança com falha morrer e reaparece uma nova. É o caso do backend pré-fork do Apache. Quando uma instância httpd falha, o pior caso é que a solicitação HTTP específica pode ser descartada para esse processo, mas o Apache gera um novo filho e, muitas vezes, a solicitação, se apenas for reenviado e atendido. O resultado final é que o Apache como um todo não é eliminado com o encadeamento defeituoso.
Outra consideração nesse cenário são vazamentos de memória. Existem alguns casos em que você pode lidar com um encadeamento de falhas de encadeamento (no UNIX, a recuperação de alguns sinais específicos - até segfault / fpviolation - é possível), mas mesmo nesse caso, você pode ter vazado toda a memória alocada por esse encadeamento (malloc, novo etc.). Assim, enquanto o processo pode permanecer, ele vaza mais e mais memória ao longo do tempo a cada falha / recuperação. Novamente, existem maneiras de minimizar isso, como o uso de conjuntos de memória do Apache. Mas isso ainda não protege contra a memória que pode ter sido alocada por bibliotecas de terceiros que o encadeamento pode estar usando.
E, como algumas pessoas apontaram, entender as primitivas de sincronização é talvez a coisa mais difícil de se acertar. Esse problema por si só - apenas acertar a lógica geral para todo o seu código - pode ser uma enorme dor de cabeça. Os deadlocks misteriosos tendem a acontecer nos momentos mais estranhos e, às vezes, até o programa estar em produção, o que torna a depuração ainda mais difícil. Adicione a isso o fato de que as primitivas de sincronização geralmente variam amplamente com a plataforma (Windows vs. POSIX), e a depuração pode ser mais difícil, além da possibilidade de condições de corrida a qualquer momento (inicialização / inicialização, tempo de execução e desligamento), programar com threads realmente tem pouca piedade para iniciantes. E mesmo para especialistas, ainda há pouca misericórdia apenas porque o conhecimento do encadeamento em si não minimiza a complexidade em geral. Às vezes, cada linha de código encadeado parece exponencialmente aumentar a complexidade geral do programa, além de aumentar a probabilidade de surgir um impasse oculto ou uma condição estranha de corrida a qualquer momento. Também pode ser muito difícil escrever casos de teste para descobrir essas coisas.
É por isso que alguns projetos como o Apache e o PostgreSQL são, em grande parte, baseados em processos. O PostgreSQL executa todos os threads de back-end em um processo separado. É claro que isso ainda não alivia o problema das condições de sincronização e corrida, mas adiciona bastante proteção e, de certa forma, simplifica as coisas.
Vários processos, cada um executando um único encadeamento de execução, podem ser muito melhores do que vários encadeamentos executados em um único processo. E com o advento de grande parte do novo código ponto a ponto como AMQP (RabbitMQ, Qpid etc.) e ZeroMQ, é muito mais fácil dividir threads em diferentes espaços de processo e até máquinas e redes, simplificando bastante as coisas. Mas ainda assim, não é uma bala de prata. Ainda há complexidade para lidar. Você acabou de mover algumas de suas variáveis do espaço do processo para a rede.
A conclusão é que a decisão de entrar no domínio dos threads não é clara. Depois de entrar nesse território, quase instantaneamente tudo se torna mais complexo e novas raças de problemas entram em sua vida. Pode ser divertido e legal, mas é como energia nuclear - quando as coisas dão errado, elas podem ir mal e rápido. Lembro-me de fazer uma aula de treinamento de crítica há muitos anos e eles mostraram fotos de alguns cientistas de Los Alamos que brincavam com plutônio nos laboratórios da Segunda Guerra Mundial. Muitos tomaram poucas ou nenhuma precaução contra o evento de uma exposição e, num piscar de olhos - em um único flash brilhante e indolor, tudo acabaria para eles. Dias depois eles estavam mortos. Richard Feynman mais tarde se referiu a isso como " fazendo cócegas na cauda do dragão. "É o tipo de brincadeira com tópicos (pelo menos para mim, pelo menos). Parece bastante inócuo no começo, e quando você é mordido, você está coçando a cabeça com a rapidez com que as coisas azedaram. Mas pelo menos os tópicos venceram te matar.
Primeiro, um único aplicativo encadeado nunca tirará vantagem de uma CPU multi-core ou hyper-threading. Mas, mesmo em um único núcleo, a CPU de rosca única, com multiencadeamento, tem vantagens.
Considere a alternativa e se isso a faz feliz. Suponha que você tenha várias tarefas que precisam ser executadas simultaneamente. Por exemplo, você deve continuar se comunicando com dois sistemas diferentes. Como você faz isso sem multi-threading? Você provavelmente criaria seu próprio agendador e deixaria chamar as diferentes tarefas que precisam ser executadas. Isso significa que você precisa dividir suas tarefas em partes. Você provavelmente precisa atender a algumas restrições em tempo real, para garantir que suas peças não ocupem muito tempo. Caso contrário, o cronômetro expirará em outras tarefas. Isso dificulta a divisão de uma tarefa. Quanto mais tarefas você precisar gerenciar, mais dividido será necessário e mais complexo o seu agendador se tornará para atender a todas as restrições.
Quando você tem vários threads, a vida pode se tornar mais fácil. Um planejador preventivo pode parar um encadeamento a qualquer momento, manter seu estado e reiniciar outro. Ele será reiniciado quando o seu thread chegar. Vantagens: a complexidade de escrever um agendador já foi feita para você e você não precisa dividir suas tarefas. Além disso, o planejador é capaz de gerenciar processos / encadeamentos que você nem conhece. E também, quando um encadeamento não precisa fazer nada (está aguardando algum evento), ele não ocupará ciclos de CPU. Isso não é tão fácil de realizar quando você está criando seu planejador de thread único inativo. (colocar algo para dormir não é tão difícil, mas como ele acorda?)
A desvantagem do desenvolvimento multithread é que você precisa entender sobre problemas de simultaneidade, estratégias de bloqueio e assim por diante. Desenvolver código multiencadeado sem erros pode ser bastante difícil. E a depuração pode ser ainda mais difícil.
existe algo que só pode ser realizado usando vários threads?
Sim. Você não pode executar código em várias CPUs ou núcleos de CPU com um único encadeamento.
Sem várias CPUs / núcleos, os threads ainda podem simplificar o código que é executado conceitualmente em paralelo, como o tratamento do cliente em um servidor - mas você pode fazer o mesmo sem os threads.
Os threads não são apenas sobre velocidade, mas sobre simultaneidade.
Se você não possui um aplicativo em lote como o @Peter sugeriu, mas um kit de ferramentas da GUI como o WPF, como você pode interagir com os usuários e a lógica de negócios com apenas um thread?
Além disso, suponha que você esteja construindo um servidor Web. Como você atenderia mais de um usuário simultaneamente com apenas um thread (supondo que nenhum outro processo)?
Existem muitos cenários em que apenas um thread simples não é suficiente. É por isso que avanços recentes, como o processador Intel MIC, com mais de 50 núcleos e centenas de threads, estão ocorrendo.
Sim, programação paralela e simultânea é difícil. Mas necessário.
O Multi-Threading pode deixar a interface da GUI ainda ser responsiva durante longas operações de processamento. Sem multiencadeamento, o usuário ficaria preso assistindo a um formulário bloqueado enquanto um longo processo está sendo executado.
O código multithread pode bloquear a lógica do programa e acessar dados obsoletos de maneiras que um único thread não pode.
Os threads podem pegar um bug obscuro de algo que um programador comum pode depurar e movê-lo para o reino, onde são contadas histórias sobre a sorte necessária para pegar o mesmo bug com suas calças quando um programador de alerta estava olhando apenas o momento certo.
os aplicativos que lidam com o bloqueio de E / S que também precisam permanecer responsivos a outras entradas (a GUI ou outras conexões) não podem ser executados individualmente
a adição de métodos de verificação na lib de E / S para ver o quanto pode ser lido sem bloqueio pode ajudar isso, mas poucas bibliotecas oferecem garantias completas sobre isso
Muitas respostas boas, mas não tenho certeza de nenhuma frase exatamente como eu faria - Talvez isso ofereça uma maneira diferente de ver:
Threads são apenas uma simplificação de programação como Objects ou Actors ou for loops (Sim, qualquer coisa que você implemente com loops, você pode implementar com if / goto).
Sem threads, você simplesmente implementa um mecanismo de estado. Eu tive que fazer isso muitas vezes (a primeira vez que eu fiz isso, eu nunca tinha ouvido falar sobre isso - apenas fiz uma grande declaração de switch controlada por uma variável "State"). Máquinas de estado ainda são bastante comuns, mas podem ser irritantes. Com os fios, uma grande parte do clichê desaparece.
Eles também tornam mais fácil para um idioma dividir a execução em tempo de execução em blocos compatíveis com várias CPUs (o mesmo acontece com os atores, acredito).
Java fornece threads "verdes" em sistemas nos quais o sistema operacional não oferece QUALQUER suporte de segmentação. Nesse caso, é mais fácil ver que eles claramente nada mais são do que uma abstração de programação.
Os sistemas operacionais usam o conceito de divisão de tempo, em que cada thread recebe a hora de executar e é antecipada. Uma abordagem como essa pode substituir a segmentação como está agora, mas escrever seus próprios planejadores em cada aplicativo seria um exagero. Além disso, você teria que trabalhar com dispositivos de E / S e assim por diante. E exigiria algum suporte do lado do hardware, para que você pudesse acionar interrupções para que seu agendador funcionasse. Basicamente, você escreveria um novo sistema operacional toda vez.
Em geral, o encadeamento pode melhorar o desempenho nos casos em que os encadeamentos aguardam a E / S ou estão em suspensão. Também permite criar interfaces que sejam responsivas e permitir processos de parada enquanto você executa tarefas longas. E também, o encadeamento aprimora as coisas em verdadeiras CPUs multicore.
Primeiro, os threads podem fazer duas ou mais coisas ao mesmo tempo (se você tiver mais de um núcleo). Embora você também possa fazer isso com vários processos, algumas tarefas simplesmente não se distribuem muito bem por vários processos.
Além disso, algumas tarefas possuem espaços que você não pode evitar facilmente. Por exemplo, é difícil ler dados de um arquivo em disco e também fazer com que seu processo faça outra coisa ao mesmo tempo. Se sua tarefa exigir necessariamente muitos dados de leitura do disco, seu processo passará muito tempo aguardando o disco, não importa o que você faça.
Segundo, os threads podem permitir que você evite otimizar grandes quantidades de seu código que não são críticas para o desempenho. Se você tiver apenas um único encadeamento, cada pedaço de código é crítico para o desempenho. Se bloquear, você estará afundado - nenhuma tarefa que seria executada por esse processo poderá avançar. Com os encadeamentos, um bloco afetará apenas esse encadeamento e outros encadeamentos poderão surgir e trabalhar em tarefas que precisam ser executadas por esse processo.
Um bom exemplo é o código de tratamento de erros executado com pouca frequência. Digamos que uma tarefa encontre um erro muito pouco frequente e o código para lidar com esse erro precise paginar na memória. Se o disco estiver ocupado e o processo tiver apenas um único encadeamento, nenhum avanço poderá ser feito até que o código para lidar com esse erro possa ser carregado na memória. Isso pode causar uma resposta explosiva.
Outro exemplo é se você raramente precisar fazer uma pesquisa no banco de dados. Se você esperar a resposta do banco de dados, seu código sofrerá um grande atraso. Mas você não quer se dar ao trabalho de tornar todo esse código assíncrono, porque é tão raro que você precisa fazer essas pesquisas. Com um tópico para fazer esse trabalho, você obtém o melhor dos dois mundos. Um encadeamento para fazer este trabalho torna o desempenho não crítico como deveria ser.