Como explicar por que o multi-threading é difícil

83

Eu 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 rodar alguns threads, esperando que tudo termine, então retorne os resultados).

No momento em que você começa a se preocupar com impasses e condições de corrida, acho muito difícil, mas o chefe não parece apreciar isso - acho que ele nunca se deparou com isso. Basta colocar uma fechadura nele é praticamente a atitude.

Então, como posso apresentá-lo ou explicar por que ele pode estar subestimando as complexidades de simultaneidade, paralelismo e multitarefa? Ou talvez eu esteja errado?

Edit: Apenas um pouco sobre o que ele fez - faça um loop através de uma lista, para cada item dessa lista crie um thread que execute um comando de atualização do banco de dados baseado nas informações daquele item. Eu não tenho certeza de como ele controlou quantos threads foram executados de uma só vez, eu acho que ele deve tê-los adicionado a uma fila se houvesse muita corrida (ele não usaria um semáforo).

    
por Mr Shoubs 02.06.2011 / 11:18
fonte

10 respostas

29
  1. 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 threads, mas exponencialmente complexo, porque você precisa ter certeza todas as intercalações possíveis de instruções de máquina ainda farão a coisa certa. Um exemplo simples de uma situação de atualização perdida ou de leitura suja é muitas vezes uma grande surpresa.

  2. "Aplique um bloqueio nele" é a solução trivial ... resolve todos os seus problemas se você não está preocupado com o desempenho. Tente ilustrar quanto de desempenho seria se, por exemplo, a Amazon tivesse que bloquear toda a costa leste sempre que alguém em Atlanta pedisse um livro!

por 02.06.2011 / 11:32
fonte
79

O multitarefa é simples. Codificar um aplicativo para multi-threading é muito, muito fácil.

Há um truque simples, e isso é usar uma fila de mensagens bem projetada (fazer não rolar o seu próprio) para passar dados entre os threads.

A parte difícil é tentar que vários threads atualizem magicamente um objeto compartilhado de alguma forma. É quando se torna propenso a erros porque as pessoas não prestam atenção às condições de corrida que estão presentes.

Muitas pessoas não usam filas de mensagens e tentam atualizar objetos compartilhados e criar problemas para eles mesmos.

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 tópicos compartilham recursos de E / S. É improvável que um programa vinculado a E / S (isto é, conexões de rede, operações de arquivo ou operações de banco de dados) seja mais rápido com muitos threads.

Se você quiser ilustrar o problema de atualização de objetos compartilhados, isso é simples. Sente-se na 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ê está pronto na metade do tempo, certo?

Se o seu chefe não pensar muito e só começar, você acabará conflitando de alguma forma e ambos escrevendo respostas para a mesma fórmula. Isso não funcionou porque existe uma condição de corrida inerente entre vocês dois antes de escrever. Nada impede que você leia a mesma fórmula e substitua as respostas uma da outra.

Existem muitas maneiras de criar condições de corrida com recursos mal ou não bloqueados.

Se você quiser evitar todos os conflitos, corte o papel em uma pilha de fórmulas. Você tira um da fila, escreve a resposta e publica as respostas. Não há conflitos porque você lê a partir de uma fila de mensagens somente de um leitor.

    
por 02.06.2011 / 12:17
fonte
25

A programação multi-threaded é provavelmente a solução mais difícil para a simultaneidade. Basicamente é uma abstração de baixo nível do que a máquina realmente faz.

Há várias abordagens, como o modelo de ator ou (software) memória transacional , que são muito mais fáceis. Ou trabalhar com estruturas de dados imutáveis (como listas e árvores).

Geralmente, uma separação adequada de interesses torna o multi-threading mais fácil. Algo, isso é muitas vezes esquecido, quando as pessoas geram 20 segmentos, todos tentando processar o mesmo buffer. Use reatores onde você precisa de sincronização e geralmente passa dados entre diferentes trabalhadores com filas de mensagens.
Se você tem um bloqueio na lógica da sua aplicação, você fez algo errado.

Então, sim, tecnicamente, o multi-threading é difícil.
"Slap a lock it" é praticamente a solução menos escalável para problemas de concorrência, e na verdade derrota todo o propósito do multi-threading. O que isso faz é reverter um problema de volta para um modelo de execução não concorrente. Quanto mais você fizer isso, mais provável é que você tenha apenas um thread em execução no momento (ou 0 em um deadlock). Derrota todo o propósito.
Isso é como dizer "Resolver os problemas do 3º mundo é fácil. Basta jogar uma bomba nele". Só porque existe uma solução trivial, isso não torna o problema trivial, já que 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 feito com abstrações apropriadas. O que torna isso muito fácil de fato.

    
por 02.06.2011 / 12:21
fonte
14

Eu acho que há um ângulo não técnico para esta questão - IMO é uma questão de confiança. Nós geralmente somos solicitados a reproduzir aplicativos complexos como - ah, eu não sei - o Facebook, por exemplo. Eu cheguei à conclusão de que se você está tendo que explicar a complexidade de uma tarefa para os não iniciados / gerentes - então algo está podre na Dinamarca.

Mesmo que outros programadores ninjas possam fazer a tarefa em cinco minutos, suas estimativas sã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 estejam 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 através da conversa, mas em estabelecer uma relação de respeito mútuo.

    
por 02.06.2011 / 13:03
fonte
6

Um simples experimento mental para entender os deadlocks é o problema " filósofo de restaurantes ". Um dos exemplos que costumo usar para descrever como as condições de corrida podem ser ruins é a situação Therac 25 . p>

"Basta colocar um cadeado nele" é a mentalidade de alguém que não se deparou com erros difíceis com multi-threading. E é possível que ele pense que você esteja 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 embutido que acaba em carros).

    
por 02.06.2011 / 16:32
fonte
3

Aplicativos simultâneos 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 processo / encadeamento é executada em relação a qualquer parte de outro encadeamento. O teste é mais difícil, leva mais tempo e é improvável encontrar todos os defeitos relacionados à simultaneidade. Os defeitos, se encontrados, são muito sutis e não podem ser consistentemente reproduzidos, portanto a fixação é difícil.

Portanto, a única aplicação concorrente correta é aquela que é comprovadamente correta, algo que não é freqüentemente praticado no desenvolvimento de software. Como resultado, a resposta de S.Lot é o melhor conselho geral, já que a passagem de mensagens é relativamente fácil de provar.

    
por 02.06.2011 / 23:07
fonte
3

Resposta curta em duas palavras: NONDETERMINISMO OBSERVÁVEL

Resposta longa: Depende de qual abordagem de programação concorrente você usa, dado o seu problema. No livro Conceitos, técnicas e modelos de programação de computadores , os autores explicam claramente quatro principais abordagens práticas para escrever programas concorrentes:

  • Programação seqüencial : uma abordagem de linha de base que não tem concorrência;
  • Concorrência declarativa : utilizável quando não há não-determinismo observável;
  • Concordância de passagem de mensagens : passagem simultânea de mensagens entre várias entidades, onde cada entidade processa internamente a mensagem em seqüência;
  • Concorrência de estado compartilhado : thread atualizando objetos passivos compartilhados por meio de ações atômicas de granulação grosseira, por exemplo bloqueios, monitores e transações;

Agora, a mais fácil dessas quatro abordagens, além da programação sequencial óbvia, é a concorrência declarativa , porque os programas escritos usando essa abordagem têm nenhum não-determinismo observável. Em outras palavras, não há nenhuma condição de raça , já que 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 simultaneidade declarativa. Aqui é onde as duas últimas abordagens não tão fáceis entram em jogo. A parte não tão fácil é uma consequência do não-determinismo observável. Agora, ambos se enquadram no modelo simultâneo stateful e também são equivalentes em expressividade. Mas devido ao número cada vez maior de núcleos por CPU, parece que a indústria tem mais interesse recentemente na simultaneidade de passagem de mensagens, como pode ser visto na ascensão de bibliotecas de passagem de mensagens (por exemplo, Akka para 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 construção de aplicativos simultâneos, já que você não precisa lidar mais com bloqueios, monitores ou transações. Por outro lado, requer uma abordagem diferente para projetar a solução, ou seja, pensar de forma a compor atores hierarquicamente. Pode-se dizer que requer uma mentalidade totalmente diferente, o que novamente pode ser ainda mais difícil do que usar a simultaneidade compartilhada de 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, muitas questões podem ser evitadas.

    
por 23.08.2015 / 11:38
fonte
0

Primeiro, eu aprendi que isso poderia trazer problemas ao ver um programa simples que iniciava 2 threads e mandava os dois imprimirem no console ao mesmo tempo, de 1-100. Em vez de:

1
1
2
2
3
3
...

Você recebe algo mais assim:

1
2
1
3
2
3
...

Execute-o novamente e você poderá obter resultados totalmente diferentes.

A maioria de nós foi treinada para assumir que nosso código será executado sequencialmente. Com a maioria dos multi-threading, não podemos aceitar isso como "out of the box".

    
por 02.06.2011 / 22:15
fonte
-3

Tente usar vários martelos para esmagar um monte de unhas bem próximas de uma só vez sem alguma comunicação entre os que seguram os martelos ... (suponha que eles estejam vendados).

Encaminhe isso para a construção de uma casa.

Agora para dormir à noite, imaginando que você é o arquiteto. :)

    
por 02.06.2011 / 21:10
fonte
-3

Parte fácil: use multithreading com recursos contemporâneos de estruturas, sistemas operacionais e hardware, como semáforos, filas, contadores intertravados, tipos de caixas atômicas, etc.

Parte difícil: implemente os recursos sem usar recursos em primeiro lugar, exceto alguns poucos recursos muito limitados de hardware, contando apenas com garantias de coerência de clock em vários núcleos.

    
por 02.06.2011 / 21:16
fonte