Sou um bom programador, meu chefe também é um bom programador. Embora ele pareça subestimar algumas tarefas, como multi-threading e quão difícil pode ser (acho muito difícil para qualquer coisa mais do que executar alguns threads, aguardar que todos terminem e retornar resultados).
No momento em que você começa a se preocupar com impasses e condições de corrida, acho isso muito difícil, mas o chefe não parece gostar disso - acho que ele nunca se deparou com isso. Basta dar um tapa nele é a atitude.
Então, como posso apresentá-lo ou explicar por que ele pode estar subestimando as complexidades de simultaneidade, paralelismo e multiencadeamento? Ou talvez eu esteja errado?
Editar: um pouco sobre o que ele fez - percorre uma lista, para cada item dessa lista, crie um encadeamento que executa um comando de atualização do banco de dados com base nas informações desse item. Não sei como ele controlou quantos threads executados ao mesmo tempo, acho que ele deve tê-los adicionado a uma fila se houver muitos em execução (ele não usaria um semáforo).
Respostas:
Se você pode contar com qualquer experiência matemática, ilustre como um fluxo de execução normal que é essencialmente determinístico se torna não apenas não determinístico com vários encadeamentos, mas exponencialmente complexo, porque você precisa garantir que todas as intercalações possíveis das instruções da máquina ainda façam a coisa certa. Um exemplo simples de uma atualização perdida ou de uma situação de leitura suja é muitas vezes revelador.
"Fechar um bloqueio" é a solução trivial ... resolve todos os seus problemas se você não está preocupado com o desempenho. Tente ilustrar o impacto de um desempenho se, por exemplo, a Amazon tivesse que bloquear toda a costa leste sempre que alguém em Atlanta encomendasse um livro!
fonte
Multi-threading é simples. Codificar um aplicativo para multiencadeamento é muito, muito fácil.
Há um truque simples, e isso é usar uma fila de mensagens bem projetada ( não faça o seu próprio) para passar dados entre threads.
A parte difícil é tentar fazer com que vários threads atualizem magicamente um objeto compartilhado de alguma forma. É aí que fica propenso a erros, porque as pessoas não prestam atenção às condições da corrida presentes.
Muitas pessoas não usam filas de mensagens e tentam atualizar objetos compartilhados e criar problemas para si mesmas.
O que se torna difícil é projetar um algoritmo que funcione bem ao passar dados entre várias filas. Isso é difícil. Mas a mecânica de threads coexistentes (via filas compartilhadas) é fácil.
Além disso, observe que os threads compartilham recursos de E / S. É improvável que um programa vinculado de E / S (ou seja, conexões de rede, operações de arquivo ou operações de banco de dados) acelere com mais threads.
Se você quiser ilustrar o problema de atualização de objeto compartilhado, isso é simples. Sente-se do outro lado da mesa com um monte de cartões de papel. Anote um conjunto simples de cálculos - 4 ou 6 fórmulas simples - com muito espaço na página.
Aqui está o jogo. Cada um de vocês lê uma fórmula, escreve uma resposta e coloca um cartão com a resposta.
Cada um de vocês fará metade do trabalho, certo? Você terminou na metade do tempo, certo?
Se o seu chefe não pensar muito e apenas começar, você acabará entrando em conflito de alguma maneira e ambos escreverão respostas para a mesma fórmula. Isso não funcionou porque há uma condição de raça inerente entre vocês dois lendo antes de escrever. Nada impede que você leia a mesma fórmula e substitua as respostas uma da outra.
Existem muitas, muitas maneiras de criar condições de corrida com recursos mal ou não bloqueados.
Se você quiser evitar todos os conflitos, recorte o papel em uma pilha de fórmulas. Você tira uma da fila, anota a resposta e publica as respostas. Não há conflitos porque vocês dois leem de uma fila de mensagens de apenas um leitor.
fonte
A programação multithread é provavelmente a solução mais difícil para a simultaneidade. Basicamente, é uma abstração de nível bastante baixo do que a máquina realmente faz.
Existem várias abordagens, como o modelo do ator ou a memória transacional (de software) , que são muito mais fáceis. Ou trabalhando com estruturas de dados imutáveis (como listas e árvores).
Geralmente, uma separação adequada de preocupações facilita a multiencadeamento. Algo que muitas vezes é esquecido quando as pessoas geram 20 threads, todas tentando processar o mesmo buffer. Use reatores onde você precisar de sincronização e geralmente passe dados entre diferentes trabalhadores com filas de mensagens.
Se você tem um bloqueio na lógica do aplicativo, fez algo errado.
Então, sim, tecnicamente, o multi-threading é difícil.
"Bloquear um bloqueio" é praticamente a solução menos escalável para problemas de simultaneidade e, na verdade, derrota todo o propósito do multiencadeamento. O que ele faz é reverter um problema para um modelo de execução não simultâneo. Quanto mais você faz, maior é a probabilidade de ter apenas um thread em execução no momento (ou 0 em um impasse). Isso derrota todo o propósito.
É como dizer "Resolver os problemas do terceiro mundo é fácil. Basta jogar uma bomba nele". Só porque existe uma solução trivial, isso não torna o problema trivial, pois você se importa com a qualidade do resultado.
Mas, na prática, resolver esses problemas é tão difícil quanto qualquer outro problema de programação e é melhor realizado com abstrações apropriadas. O que facilita bastante.
fonte
Eu acho que há um ângulo não técnico nessa questão - a IMO é uma questão de confiança. Geralmente, somos solicitados a reproduzir aplicativos complexos como - oh, eu não sei - o Facebook, por exemplo. Cheguei à conclusão de que, se você está tendo que explicar a complexidade de uma tarefa para os não-iniciados / gerentes - algo está estragado na Dinamarca.
Mesmo se outros programadores ninjas puderem fazer a tarefa em 5 minutos, suas estimativas serão baseadas em sua capacidade pessoal. Seu interlocutor deve aprender a confiar em sua opinião sobre o assunto ou contratar alguém cuja palavra eles estão dispostos a aceitar.
O desafio não está em transmitir as implicações técnicas, que as pessoas tendem a ignorar ou são incapazes de compreender por meio da conversa, mas em estabelecer uma relação de respeito mútuo.
fonte
Um experimento simples para entender os impasses é o problema do " filósofo do jantar ". Um dos exemplos que costumo usar para descrever como as más condições de corrida podem ser é a situação do Therac 25 .
"Apenas dar um tapinha nele" é a mentalidade de alguém que não encontrou bugs difíceis com o multi-threading. E é possível que ele pense que você está exagerando a seriedade da situação (eu não - é possível explodir coisas ou matar pessoas com problemas de condição de corrida, especialmente com software incorporado que acaba em carros).
fonte
Aplicativos concorrentes não são determinísticos. Com a quantidade excepcionalmente pequena de código geral que o programador reconheceu como vulnerável, você não controla quando uma parte de um thread / processo é executada em relação a qualquer parte de outro thread. O teste é mais difícil, leva mais tempo e é improvável que encontre todos os defeitos relacionados à simultaneidade. Os defeitos, se encontrados, geralmente são sutis, não podem ser reproduzidos de maneira consistente; portanto, a fixação é difícil.
Portanto, o único aplicativo simultâneo correto é aquele que é comprovadamente correto, algo que muitas vezes não é praticado no desenvolvimento de software. Como resultado, a resposta da S.Lot é o melhor conselho geral, pois a passagem de mensagens é relativamente fácil de provar correta.
fonte
Resposta curta em duas palavras: NONDETERMINISMO OBSERVÁVEL
Resposta longa: depende de qual abordagem da programação simultânea você usa, devido ao seu problema. No livro Conceitos, técnicas e modelos de programação de computadores , os autores explicam claramente quatro abordagens práticas principais para escrever programas concorrentes:
Agora, a mais fácil dessas quatro abordagens, além da óbvia programação seqüencial, é a simultaneidade declarativa , porque os programas escritos usando essa abordagem não têm um não determinismo observável . Em outras palavras, não há condições de corrida , pois a condição de corrida é apenas um comportamento não determinístico observável.
Mas a falta de não determinismo observável significa que existem alguns problemas que não podemos resolver usando a simultaneidade declarativa. Aqui é onde as duas últimas abordagens não tão fáceis entram em cena. A parte não tão fácil é uma consequência do não determinismo observável. Agora, ambos se enquadram no modelo concorrente estável e também são equivalentes em expressividade. Porém, devido ao número cada vez maior de núcleos por CPU, parece que o setor se interessou mais recentemente pela simultaneidade de passagem de mensagens, como pode ser visto no surgimento de bibliotecas de passagem de mensagens (por exemplo, Akka for JVM) ou linguagens de programação (por exemplo, Erlang ) .
A biblioteca Akka mencionada anteriormente, que é apoiada por um modelo teórico de ator, simplifica a criação de aplicativos simultâneos, pois você não precisa mais lidar com bloqueios, monitores ou transações. Por outro lado, requer uma abordagem diferente para projetar a solução, ou seja, pensar em uma maneira de compor hierarquicamente os atores. Pode-se dizer que requer uma mentalidade totalmente diferente, que novamente pode ser ainda mais difícil do que usar a simultaneidade compartilhada em estado simples.
A programação simultânea é difícil por causa do não determinismo observável, mas ao usar a abordagem correta para o problema em questão e a biblioteca certa que suporta essa abordagem, muitos problemas podem ser evitados.
fonte
Foi-me ensinado pela primeira vez que isso poderia trazer à tona problemas ao ver um programa simples que iniciava 2 threads e os dois imprimiam no console ao mesmo tempo de 1 a 100. Ao invés de:
Você obtém algo mais como este:
Execute-o novamente e você poderá obter resultados totalmente diferentes.
Muitos de nós foram treinados para assumir que nosso código será executado seqüencialmente. Com a maioria dos multi-threading, não podemos tomar isso como garantido "fora da caixa".
fonte
Tente usar vários martelos para esmagar várias unhas espaçadas ao mesmo tempo, sem comunicação entre os que seguram os martelos ... (suponha que estejam com os olhos vendados).
Escale isso para a construção de uma casa.
Agora tente dormir à noite imaginando que você é o arquiteto. :)
fonte
Parte fácil: use multithreading com recursos contemporâneos de estruturas, sistemas operacionais e hardware, como semáforos, filas, contadores intertravados, tipos atômicos de caixas etc.
Parte difícil: implementar os recursos por si próprios, sem recursos em primeiro lugar, pode ser exceto poucos recursos muito limitados de hardware, contando apenas com garantias de coerência de clock em vários núcleos.
fonte