Existem práticas descontinuadas para programação multithread e multiprocessador que eu não devo mais usar?


36

Nos primeiros dias do FORTRAN e do BASIC, essencialmente todos os programas foram escritos com instruções GOTO. O resultado foi o código espaguete e a solução foi a programação estruturada.

Da mesma forma, os ponteiros podem ter características difíceis de controlar em nossos programas. O C ++ começou com muitos ponteiros, mas o uso de referências é recomendado. Bibliotecas como STL podem reduzir parte de nossa dependência. Existem também expressões idiomáticas para criar ponteiros inteligentes com melhores características, e algumas versões do C ++ permitem referências e código gerenciado.

Práticas de programação como herança e polimorfismo usam muitos ponteiros nos bastidores (assim como, durante, a programação estruturada gera código preenchido com instruções de ramificação). Idiomas como Java eliminam ponteiros e usam a coleta de lixo para gerenciar dados alocados dinamicamente, em vez de depender dos programadores para corresponder a todas as novas e excluir instruções.

Na minha leitura, vi exemplos de programação multiprocessos e multiencadeamentos que parecem não usar semáforos. Eles usam a mesma coisa com nomes diferentes ou têm novas maneiras de estruturar a proteção de recursos contra o uso simultâneo?

Por exemplo, um exemplo específico de um sistema para programação multithread com processadores multicore é o OpenMP. Representa uma região crítica da seguinte forma, sem o uso de semáforos, que parecem não estar incluídos no ambiente.

th_id = omp_get_thread_num();
#pragma omp critical
{
  cout << "Hello World from thread " << th_id << '\n';
}

Este exemplo é um trecho de: http://en.wikipedia.org/wiki/OpenMP

Como alternativa, uma proteção semelhante dos encadeamentos usando semáforos com as funções wait () e signal () pode se parecer com:

wait(sem);
th_id = get_thread_num();
cout << "Hello World from thread " << th_id << '\n';
signal(sem);

Neste exemplo, as coisas são bem simples, e apenas uma simples revisão é suficiente para mostrar que as chamadas em espera () e sinal () são correspondidas e, mesmo com muita simultaneidade, a segurança do encadeamento é fornecida. Mas outros algoritmos são mais complicados e usam vários semáforos (binários e contadores) espalhados por várias funções com condições complexas que podem ser chamadas por muitos threads. As consequências de criar um impasse ou deixar de tornar o thread seguro podem ser difíceis de gerenciar.

Esses sistemas, como o OpenMP, eliminam os problemas com semáforos?
Eles movem o problema para outro lugar?
Como transformar meu semáforo favorito usando o algoritmo para não usar mais semáforos?


Do que exatamente você está falando? O que você viu?
svick

4
Não querendo ser rude, mas você poderia ter eliminado os três primeiros parágrafos. Eles realmente não se preocupam com a sua pergunta e superam suas conclusões e apenas geram muitos argumentos.
dbracey

1
Whoa, grande edição. Eu tomei uma facada em uma resposta. A pergunta ainda meio que vagueia pelo GOTO, indicadores, herança e polimorfismo, mas na minha resposta eu deixei essas questões de lado e me concentrei na questão das "práticas obsoletas".
Stuart Marcas

Respostas:


15

Existem técnicas e práticas de programação concorrentes que não se deve mais usar? Eu diria que sim .

Uma técnica de programação simultânea inicial que parece rara atualmente é a programação orientada a interrupções . Foi assim que o UNIX funcionou na década de 1970. Veja o Comentário do Lions sobre o UNIX ou o design do sistema operacional UNIX de Bach . Resumidamente, a técnica é suspender temporariamente as interrupções enquanto manipula uma estrutura de dados e depois restaurar as interrupções. A página do manual BSD spl (9)tem um exemplo desse estilo de codificação. Observe que as interrupções são orientadas por hardware e o código incorpora um relacionamento implícito entre o tipo de interrupção de hardware e as estruturas de dados associadas a esse hardware. Por exemplo, o código que manipula os buffers de E / S de disco precisa suspender as interrupções do hardware do controlador de disco enquanto trabalha com esses buffers.

Esse estilo de programação foi empregado pelos sistemas operacionais no hardware do uniprocessador. Era muito mais raro os aplicativos lidarem com interrupções. Alguns sistemas operacionais tiveram interrupções de software, e acho que as pessoas tentaram criar sistemas de encadeamento ou corotina sobre eles, mas isso não foi muito difundido. (Certamente não no mundo UNIX.) Suspeito que a programação no estilo de interrupção esteja confinada hoje a pequenos sistemas embarcados ou sistemas em tempo real.

Os semáforos são um avanço em relação às interrupções porque são construções de software (não relacionadas ao hardware), fornecem abstrações sobre as instalações de hardware e permitem multithreading e multiprocessing. O principal problema é que eles não são estruturados. O programador é responsável por manter o relacionamento entre cada semáforo e as estruturas de dados que protege, globalmente em todo o programa. Por esse motivo, acho que os semáforos simples raramente são usados ​​hoje.

Outro pequeno passo adiante é um monitor , que encapsula mecanismos de controle de simultaneidade (bloqueios e condições) com os dados sendo protegidos. Isso foi transferido para o sistema Mesa (link alternativo) e de lá para Java. (Se você ler este documento do Mesa, poderá ver que as condições e bloqueios do monitor de Java são copiados quase literalmente do Mesa.) Os monitores são úteis, pois um programador suficientemente cuidadoso e diligente pode escrever programas concorrentes com segurança, usando apenas o raciocínio local sobre o código e os dados. dentro do monitor.

Existem construções de biblioteca adicionais, como as do java.util.concurrentpacote Java , que incluem uma variedade de estruturas de dados altamente simultâneas e construções de pool de encadeamentos. Eles podem ser combinados com técnicas adicionais, como confinamento de linhas e imutabilidade efetiva. Veja Java Concurrency In Practice, de Goetz et. al. para uma discussão mais aprofundada. Infelizmente, muitos programadores ainda estão rolando suas próprias estruturas de dados com bloqueios e condições, quando realmente deveriam usar algo como ConcurrentHashMap, onde o trabalho pesado já foi feito pelos autores da biblioteca.

Tudo o que está acima compartilha algumas características significativas: eles têm vários threads de controle que interagem em um estado mutável compartilhado globalmente . O problema é que a programação nesse estilo ainda é altamente suscetível a erros. É muito fácil um pequeno erro passar despercebido, resultando em mau comportamento que é difícil de reproduzir e diagnosticar. Pode ser que nenhum programador seja "suficientemente cuidadoso e diligente" para desenvolver grandes sistemas dessa maneira. Pelo menos, muito poucos são. Então, eu diria que a programação multithread com estado compartilhado e mutável deve ser evitada, se possível.

Infelizmente, não está totalmente claro se isso pode ser evitado em todos os casos. Muita programação ainda é feita dessa maneira. Seria bom ver isso suplantado por outra coisa. As respostas de Jarrod Roberson e davidk01 apontam para técnicas como dados imutáveis, programação funcional, STM e passagem de mensagens. Há muito para recomendá-los e todos estão sendo desenvolvidos ativamente. Mas não acho que eles tenham substituído totalmente o bom estado mutável compartilhado à moda antiga ainda.

EDIT: aqui está a minha resposta para as perguntas específicas no final.

Eu não sei muito sobre o OpenMP. Minha impressão é que pode ser muito eficaz para problemas altamente paralelos, como simulações numéricas. Mas isso não parece de propósito geral. As construções de semáforo parecem bem de baixo nível e exigem que o programador mantenha o relacionamento entre semáforos e estruturas de dados compartilhadas, com todos os problemas que descrevi acima.

Se você tem um algoritmo paralelo que usa semáforos, não conheço nenhuma técnica geral para transformá-lo. Você pode refatorá-lo em objetos e criar algumas abstrações em torno dele. Mas se você quiser usar algo como passagem de mensagens, acho que realmente precisará reconceituar todo o problema.


Obrigado, esta é uma ótima informação. Examinarei as referências e aprofundarei os conceitos que você menciona que são novos para mim.
DeveloperDon

+1 para java.util.concurrent e concordou com o comentário - ele está no JDK desde a versão 1.5 e eu raramente o vejo usado.
MebAlone 20/09/12

1
Desejo que você destaque como é importante não criar suas próprias estruturas quando já existem. Tantos, tantos erros ...
corsiKa

Não acho correto dizer: "Os semáforos são um avanço em relação às interrupções porque são construções de software (não relacionadas ao hardware) ". Os semáforos dependem da CPU para implementar a instrução Compare-and-Swap , ou são variantes com vários núcleos .
Josh Pearce

@JoshPearce de semáforos curso são implementados usando construções de hardware, mas eles são uma abstração que é independente de qualquer construção hardware específico, como CAS, test-and-set, cmpxchng, etc.
Stuart Marcas

28

Responda a pergunta

O consenso geral é que o estado mutável compartilhado é Bad ™ e o estado imutável é Good ™, que é provado ser preciso e verdadeiro repetidamente pelas linguagens funcionais e linguagens imperativas.

O problema é que os idiomas imprescindíveis não são projetados para lidar com esse modo de trabalhar, as coisas não vão mudar para esses idiomas durante a noite. É aqui que a comparação GOTOé falha. O estado imutável e a passagem de mensagens são uma ótima solução, mas também não são uma panacéia.

Premissa imperfeita

Esta questão é baseada em comparações com uma premissa falhada; esse GOTOera o problema real e foi universalmente reprovado de alguma forma pelo Conselho Universal Intergalático de Designers de Idiomas e Uniões de Engenharia de Software ©! Sem um GOTOmecanismo, o ASM não funcionaria. O mesmo com a premissa de que ponteiros brutos são o problema com C ou C ++ e, de certa forma, como ponteiros inteligentes são uma panacéia, eles não são.

GOTONão era o problema, os programadores eram o problema. O mesmo vale para o estado mutável compartilhado . Por si só não é o problema , são os programadores que o utilizam. Se houvesse uma maneira de gerar código que usasse o estado mutável compartilhado de uma maneira que nunca tivesse condições de corrida ou bugs, isso não seria um problema. Assim como se você nunca escreve código espaguete com GOTOconstruções equivalentes, também não é um problema.

A educação é a panacéia

Programadores idiotas são o que eram deprecated: toda linguagem popular ainda tem o GOTOconstruto, direta ou indiretamente, e é usadabest practice quando usada corretamente em todo idioma que possui esse tipo de construto.

EXEMPLO: Java possui rótulos e try/catch/finallyambos funcionam diretamente como GOTOinstruções.

A maioria dos programadores de Java com quem converso nem sabe o que immutablerealmente significa fora deles, repetindo the String class is immutablecom um zumbi como o olhar nos olhos. Definitivamente, eles não sabem como usar a finalpalavra - chave corretamente para criar uma immutableclasse. Portanto, tenho certeza de que eles não têm idéia de por que a passagem de mensagens usando mensagens imutáveis ​​é tão boa e por que o estado mutável compartilhado não é tão bom.


3
+1 Ótima resposta, claramente escrita e identificando o padrão subjacente do estado mutável. IUBLDSEU deve tornar-se um meme :)
Dibbeke

2
GOTO é uma palavra de código para 'por favor, não, por favor, comece uma guerra de chamas aqui, eu duvido você'. Esta pergunta apaga as chamas, mas não dá uma boa resposta. Menções honrosas de programação funcional e imutabilidade são ótimas, mas não há carne para essas declarações.
Evan Solha

1
Esta parece ser uma resposta contraditória. Primeiro, você diz "A é ruim, B é bom", depois diz "Idiotas foram preteridos". O mesmo não se aplica ao primeiro parágrafo? Não posso simplesmente pegar a última parte da sua resposta e dizer "O estado mutável compartilhado é uma prática recomendada quando usado corretamente em todos os idiomas". Além disso, "prova" é uma palavra muito forte. Você não deve usá-lo, a menos que tenha evidências realmente fortes.
luiscubal 18/08/12

2
Não era minha intenção iniciar uma guerra de chamas. Até Jarrod reagir ao meu comentário, pensara que o GOTO não era controverso e funcionaria bem em analogia. Quando escrevi a pergunta, isso não me ocorreu, mas Dijkstra estava no ponto zero nos GOTOs e nos semáforos. Edsger Dijkstra parece um gigante para mim, e foi creditado com a invenção dos semáforos (1965) e início (1968) do trabalho acadêmico sobre GOTOs. O método de defesa de Dijkstra era frequentemente duro e de confronto. A controvérsia / confronto funcionou para ele, mas eu só quero idéias sobre possíveis alternativas aos semáforos.
DeveloperDon

1
Muitos programas devem modelar coisas que, no mundo real, são mutáveis. Se às 5h37 da manhã, o objeto 451 contiver o estado de algo no mundo real naquele momento (5h37), e o estado da coisa do mundo real mudar posteriormente, é possível a identidade do objeto que representa o estado da coisa do mundo real a ser imutável (ou seja, a coisa sempre será representada pelo objeto # 451) ou o objeto # 451 a ser imutável, mas não ambos. Em muitos casos, ter a identidade imutável será mais útil do que o objeto nº 451 ser imutável.
Supercat

27

A última moda nos círculos acadêmicos parece ser a Memória Transacional de Software (STM) e promete tirar todos os detalhes cabeludos da programação multithread das mãos dos programadores, usando a tecnologia de compilador suficientemente inteligente. Nos bastidores, ainda existem bloqueios e semáforos, mas você, como programador, não precisa se preocupar com isso. Os benefícios dessa abordagem ainda não são claros e não há concorrentes óbvios.

Erlang usa a passagem de mensagens e agentes para simultaneidade e esse é um modelo mais simples de trabalhar do que o STM. Com a passagem de mensagens, você não tem absolutamente nenhum bloqueio e semáforo para se preocupar, pois cada agente opera em seu próprio mini universo, portanto, não há condições de corrida relacionadas a dados. Você ainda tem alguns casos extremos estranhos, mas eles não são nem de longe tão complicados quanto bloqueios e bloqueios. As linguagens da JVM podem usar o Akka e obter todos os benefícios da passagem de mensagens e dos atores, mas, diferentemente do Erlang, a JVM não possui suporte interno para os atores, portanto, no final do dia, o Akka ainda usa threads e bloqueios, mas você como o programador não precisa se preocupar com isso.

O outro modelo que eu conheço que não usa bloqueios e threads é o uso de futuros, que na verdade é apenas outra forma de programação assíncrona.

Não tenho certeza de quanto dessa tecnologia está disponível no C ++, mas é provável que, se você estiver vendo algo que não esteja explicitamente usando threads e bloqueios, será uma das técnicas acima para gerenciar a simultaneidade.


+1 para o novo termo "detalhes peludos". LOL cara. Eu simplesmente não consigo parar de rir deste novo termo. Acho que vou usar "código peludo" a partir de agora.
Saeed Neamati 16/08/2012

1
@ Saeed: Eu já ouvi essa expressão antes, não é tão incomum. Concordo que é que engraçado :-)
Cameron

1
Boa resposta. A CLI do .NET supostamente também tem suporte para sinalização (em oposição ao bloqueio), mas ainda não encontrei um exemplo em que substituísse completamente o bloqueio. Não tenho certeza se o assíncrono conta. Se você está falando de plataformas como Javascript / NodeJs, elas são realmente de thread único e só são melhores com altas cargas de IO, porque são muito menos suscetíveis a maximizar os limites de recursos (por exemplo, em vários contextos descartáveis). Em cargas intensivas da CPU, há pouco / nenhum benefício em usar a programação assíncrona.
Evan Solha

1
Resposta interessante, eu não tinha encontrado futuros antes. Observe também que você ainda pode ter deadlock e livelock em sistemas de passagem de mensagens como Erlang . O CSP permite que você raciocine formalmente sobre deadlock e livelock, mas não o impede por si só.
Mark Booth

1
Eu adicionaria o Lock free e esperaria estruturas de dados gratuitas a essa lista.
Stonemetal

3

Eu acho que isso é principalmente sobre níveis de abstração. Muitas vezes, na programação, é útil abstrair alguns detalhes de uma maneira que seja mais segura ou legível ou algo assim.

Isso se aplica a estruturas de controle: ifs, fors e até mesmo try- catchblocos são abstrações pouco mais de gotos. Essas abstrações são quase sempre úteis, porque tornam seu código mais legível. Mas há casos em que você ainda precisará usar goto(por exemplo, se estiver escrevendo a montagem manualmente).

Isso também se aplica ao gerenciamento de memória: ponteiros inteligentes C ++ e GC são abstrações sobre ponteiros brutos e desalocação / alocação manual de memória. E, às vezes, essas abstrações não são apropriadas, por exemplo, quando você realmente precisa de desempenho máximo.

E o mesmo se aplica à multithread: coisas como futuros e atores são apenas abstrações sobre threads, semáforos, mutexes e instruções CAS. Essas abstrações podem ajudá-lo a tornar seu código muito mais legível e também a evitar erros. Mas, às vezes, eles simplesmente não são apropriados.

Você deve saber quais ferramentas você tem disponível e quais são suas vantagens e desvantagens. Em seguida, você pode escolher a abstração correta para sua tarefa (se houver). Níveis mais altos de abstração não depreciam os níveis mais baixos, sempre haverá alguns casos em que a abstração não é apropriada e a melhor opção é usar o “modo antigo”.


Obrigado, você está entendendo a analogia, e eu não tenho uma idéia preconcebida ou mesmo machado sobre se a resposta do WRT é semáforo. As questões maiores para mim são: existem maneiras melhores e em sistemas que parecem não ter semáforos faltando algo importante e seriam incapazes de executar toda a gama de algoritmos multithread.
DeveloperDon

2

Sim, mas é provável que você não encontre alguns deles.

Antigamente, era comum o uso de métodos de bloqueio (sincronização de barreira), porque escrever bons mutexes era difícil de fazer corretamente. Você ainda pode ver traços disso em coisas recentes. O uso de modernas bibliotecas de simultaneidade fornece um conjunto de ferramentas muito mais rico e completamente testado para paralelização e coordenação entre processos.

Da mesma forma, uma prática mais antiga era escrever código torturante para que você pudesse descobrir como paralelizar manualmente. Essa forma de otimização (potencialmente prejudicial, se você errar) também saiu pela janela com o advento de compiladores que fazem isso por você, desenrolando loops, se necessário, seguindo preditivamente ramificações, etc. Essa não é uma nova tecnologia. , estar pelo menos 15 anos no mercado. Tirar proveito de coisas como conjuntos de encadeamentos também contorna algum código realmente complicado do passado.

Portanto, talvez a prática descontinuada seja escrever o código de concorrência, em vez de usar bibliotecas modernas e bem testadas.


Obrigado. Parece que existe um grande potencial para o uso de programação simultânea, mas pode ser uma caixa de Pandora se não for usada de maneira disciplinada.
DeveloperDon

2

O Grand Central Dispatch da Apple é uma abstração elegante que mudou meu pensamento sobre simultaneidade. Seu foco nas filas torna a implementação da lógica assíncrona uma ordem de magnitude mais simples, na minha humilde experiência.

Quando programa em ambientes onde está disponível, ele substitui a maioria dos meus usos de threads, bloqueios e comunicação entre threads.


1

Uma das principais mudanças na programação paralela é que as CPUs são tremendamente mais rápidas do que antes, mas para alcançar esse desempenho, é necessário um cache bem preenchido. Se você tentar executar vários threads ao mesmo tempo trocando entre eles continuamente, você quase sempre invalidará o cache de cada segmento (ou seja, cada segmento requer dados diferentes para operar) e acabará prejudicando o desempenho muito mais do que você acostumado com CPUs mais lentas.

Essa é uma das razões pelas quais as estruturas assíncronas ou baseadas em tarefas (por exemplo, Grand Central Dispatch ou TBB da Intel) são mais populares, executam tarefas de código 1 de cada vez, concluindo-as antes de passar para a próxima - no entanto, você deve codificar cada cada tarefa leva pouco tempo, a menos que você queira estragar o design (ou seja, suas tarefas paralelas estão realmente na fila). As tarefas com uso intenso de CPU são passadas para um núcleo de CPU alternativo, em vez de processadas no thread único que processa todas as tarefas. Também é mais fácil de gerenciar, se não houver um processamento verdadeiramente multiencadeado.


Legal, obrigado por referências à tecnologia Apple e Intel. Sua resposta está apontando desafios para gerenciar o encadeamento de afinidades principais? Alguns problemas de desempenho de cache são atenuados porque processadores multicore podem repetir caches L1 por núcleo? Por exemplo: software.intel.com/en-us/articles/… O cache de alta velocidade para quatro núcleos com mais ocorrências de cache pode ser mais de 4x mais rápido que um núcleo com mais falhas de cache nos mesmos dados. A multiplicação de matrizes pode. O agendamento aleatório de 32 threads em 4 núcleos não pode. Vamos usar afinidade e obter 32 núcleos.
DeveloperDon

na verdade, embora não seja o mesmo problema - a afinidade principal apenas se refere ao problema em que uma tarefa é saltada de um núcleo para o outro. É o mesmo problema se uma tarefa for interrompida, substituída por uma nova, e a tarefa original continuará no mesmo núcleo. A Intel está dizendo: cache atinge = rápido, cache perde = lento, independentemente do número de núcleos. Eu acho que eles estão tentando persuadi-lo a comprar seus chips em vez de AMD :)
gbjbaanb
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.