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?
fonte
Respostas:
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.concurrent
pacote 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.
fonte
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
GOTO
era 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 umGOTO
mecanismo, 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.GOTO
Nã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 comGOTO
construçõ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 oGOTO
construto, 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/finally
ambos funcionam diretamente comoGOTO
instruções.A maioria dos programadores de Java com quem converso nem sabe o que
immutable
realmente significa fora deles, repetindothe String class is immutable
com um zumbi como o olhar nos olhos. Definitivamente, eles não sabem como usar afinal
palavra - chave corretamente para criar umaimmutable
classe. 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.fonte
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.
fonte
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:
if
s,for
s e até mesmotry
-catch
blocos são abstrações pouco mais degoto
s. Essas abstrações são quase sempre úteis, porque tornam seu código mais legível. Mas há casos em que você ainda precisará usargoto
(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”.
fonte
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.
fonte
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.
fonte
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.
fonte