Que lições você aprendeu de um projeto que quase / realmente falhou devido a um multithreading ruim? [fechadas]


11

Que lições você aprendeu de um projeto que quase / realmente falhou devido a um multithreading ruim?

Às vezes, a estrutura impõe um certo modelo de encadeamento que torna as coisas em uma ordem de magnitude mais difíceis de acertar.

Quanto a mim, ainda estou para me recuperar da última falha e sinto que é melhor não trabalhar em nada que tenha a ver com multithreading nesse quadro.

Eu descobri que era bom em problemas de multithreading que possuem simples junção / forquilha e onde os dados trafegam apenas em uma direção (enquanto os sinais podem trafegar em uma direção circular).

Não consigo lidar com a GUI na qual algum trabalho pode ser feito apenas em um thread estritamente serializado (o "thread principal") e outro trabalho só pode ser feito em qualquer thread, exceto o thread principal (os "threads de trabalho") e onde dados e mensagens precisam viajar em todas as direções entre N componentes (um gráfico totalmente conectado).

Na época em que deixei esse projeto para outro, havia problemas de impasse por toda parte. Ouvi dizer que, 2-3 meses depois, vários outros desenvolvedores conseguiram corrigir todos os problemas de conflito, a ponto de poderem ser enviados aos clientes. Eu nunca consegui descobrir o conhecimento que faltava.

Algo sobre o projeto: o número de IDs de mensagens (valores inteiros que descrevem o significado de um evento que pode ser enviado para a fila de mensagens de outro objeto, independentemente do encadeamento), chega a vários milhares. Seqüências de caracteres únicas (mensagens de usuário) também chegam a cerca de mil.

Adicionado

A melhor analogia que recebi de outra equipe (não relacionada aos meus projetos passados ​​ou presentes) foi "colocar os dados em um banco de dados". ("Banco de dados" referente à centralização e atualizações atômicas.) Em uma GUI fragmentada em várias visualizações, todas em execução no mesmo "encadeamento principal" e todo o levantamento pesado não-GUI é feito em encadeamentos de trabalho individuais, os dados do aplicativo devem seja armazenado em um único local que atue como um banco de dados e deixe o "banco de dados" lidar com todas as "atualizações atômicas" que envolvem dependências de dados não triviais. Todas as outras partes da GUI lidam apenas com o desenho da tela e nada mais. As partes da interface do usuário podem armazenar em cache coisas e o usuário não notará se estiver obsoleto por uma fração de segundo, se for projetado corretamente. Esse "banco de dados" também é conhecido como "o documento" na arquitetura Document-View. Infelizmente, não, meu aplicativo realmente armazena todos os dados nas Views. Não sei por que foi assim.

Colaboradores:

(os colaboradores não precisam usar exemplos reais / pessoais. As lições de exemplos anedóticos, se você julgar credível, também serão bem-vindas.)



Eu acho que ser capaz de "pensar em tópicos" é um pouco de talento e menos algo que pode ser aprendido, por falta de uma redação melhor. Conheço muitos desenvolvedores que trabalham com sistemas paralelos há muito tempo, mas eles engasgam se os dados precisam ir em mais de uma direção.
Dauphic 9/11

Respostas:


13

Minha lição favorita - ganhou muito! - é que em um programa multithread o agendador é um porco furtivo que o odeia. Se as coisas derem errado, eles vão, mas de uma maneira inesperada. Cometa algo errado e você estará perseguindo erros estranhos de heisen (porque qualquer instrumentação que você adicionar alterará os tempos e fornecerá um padrão de execução diferente).

A única maneira sensata de corrigir isso é restringir rigorosamente todo o manuseio de encadeamentos em um pequeno pedaço de código que funcione corretamente e que seja muito conservador quanto à garantia de que os bloqueios sejam mantidos adequadamente (e com uma ordem de aquisição globalmente constante também) . A maneira mais fácil de fazer isso é não compartilhar memória (ou outros recursos) entre threads, exceto para mensagens que devem ser assíncronas; que permite que você escreva todo o resto em um estilo que não reconhece os threads. (Bônus: expandir para várias máquinas em um cluster é muito mais fácil.)


+1 para "não compartilhar memória (ou outros recursos) entre threads, exceto para mensagens que devem ser assíncronas;"
Nemanja Trifunovic

1
O único caminho? E quanto aos tipos de dados imutáveis?
Aaronaught

is that in a multithreaded program the scheduler is a sneaky swine that hates you.- não, não, ele faz exatamente o que você disse para fazer :)
mattnz

@Aaronaught: Os valores globais passados ​​por referência, mesmo que imutáveis, ainda exigem GC global e que reintroduz um monte de recursos globais. Ser capaz de usar o gerenciamento de memória por thread é bom, pois permite que você se livre de vários bloqueios globais.
Donal Fellows

Não é que você não possa passar valores de tipos não básicos por referência, mas isso exige níveis mais altos de bloqueio (por exemplo, o "proprietário" mantendo uma referência até que alguma mensagem volte, o que é fácil prejudicar na manutenção) ou código complexo no mecanismo do sistema de mensagens para transferir a propriedade. Ou você empacota tudo e desempacota no outro encadeamento, o que é muito mais lento (você precisa fazer isso ao acessar um cluster de qualquer maneira). Ir direto ao assunto e não compartilhar memória é mais fácil.
Donal Fellows

6

Aqui estão algumas lições básicas em que posso pensar agora (não em projetos com falha, mas com problemas reais vistos em projetos reais):

  • Tente evitar chamadas bloqueadas enquanto mantém um recurso compartilhado. O padrão de deadlock comum é o segmento que agarra o mutex, faz um retorno de chamada e bloqueia o mesmo no mutex.
  • Proteja o acesso a qualquer estrutura de dados compartilhada com uma seção mutex / crítica (ou use estruturas livres de bloqueio - mas não invente a sua!)
  • Não assuma atomicidade - use APIs atômicas (por exemplo, InterlockedIncrement).
  • RTFM referente à segurança de thread de bibliotecas, objetos ou APIs que você está usando.
  • Aproveite as primitivas de sincronização disponíveis, por exemplo, eventos, semáforos. (Mas preste muita atenção ao usá-los, pois você sabe que está em bom estado - já vi muitos exemplos de eventos sinalizados no estado errado, para que eventos ou dados possam se perder)
  • Suponha que os threads possam ser executados simultaneamente e / ou em qualquer ordem e que o contexto possa alternar entre os threads a qualquer momento (a menos que em um SO que ofereça outras garantias).

6
  • Todo o seu projeto GUI deve ser chamado apenas a partir do encadeamento principal . Basicamente, você não deve colocar um único ".net" na sua GUI. O multithreading deve ficar preso em projetos separados que lidam com o acesso mais lento aos dados.

Herdamos uma parte em que o projeto GUI está usando uma dúzia de threads. Está dando nada além de problemas. Impasses, problemas de corrida, chamadas GUI de discussão cruzada ...


"Projeto" significa "montagem"? Não vejo como a distribuição de classes entre assemblies causaria problemas de segmentação.
Nikie

No meu projeto é de fato uma montagem. Mas o ponto principal é que todo o código nessas pastas deve ser chamado a partir do thread principal, sem exceções.
Carra

Eu não acho que essa regra seja geralmente aplicável. Sim, você nunca deve chamar o código da GUI de outro segmento. Mas como você distribui classes para pastas / projetos / montagens é uma decisão independente.
Nikie

1

O Java 5 e posteriores têm Executors que visam facilitar a vida no manuseio de programas no estilo de junção de forquilha com vários threads.

Use-os, isso removerá muita dor.

(e, sim, isso eu aprendi com um projeto :))


1
Para aplicar esta resposta a outros idiomas - use estruturas de processamento paralelo de alta qualidade fornecidas por esse idioma sempre que possível. (No entanto, só o tempo dirá se um quadro é realmente grande e altamente utilizável.)
rwong

1

Tenho experiência em sistemas embarcados em tempo real. Você não pode testar a ausência de problemas causados ​​pelo multithreading. (Às vezes você pode confirmar a presença). O código deve estar comprovadamente correto. Portanto, práticas recomendadas para toda e qualquer interação de encadeamento.

  • Regra nº 1: BEIJO - Se não precisar de um tópico, não gire um. Serialize o máximo possível.
  • Regra nº 2: não quebre a nº 1.
  • # 3 Se você não pode provar através da revisão que está correto, não é.

+1 para a regra 1. Eu estava trabalhando em um projeto que inicialmente iria bloquear até que outro thread fosse concluído - essencialmente uma chamada de método! Felizmente, decidimos contra essa abordagem.
Michael K

# 3 FTW. É melhor passar horas lutando com diagramas de tempo de bloqueio ou o que quer que você use para provar que é bom que meses se perguntando por que às vezes desmorona.

1

Uma analogia de uma aula sobre multithreading que fiz no ano passado foi muito útil. A sincronização de threads é como um sinal de tráfego que protege uma interseção (dados) de ser usada por dois carros (threads) de uma só vez. O erro que muitos desenvolvedores cometem é acender luzes vermelhas na maior parte da cidade para deixar um carro passar, porque eles acham que é muito difícil ou perigoso descobrir o sinal exato de que precisam. Isso pode funcionar bem quando o tráfego é baixo, mas leva a um impasse à medida que o aplicativo cresce.

Isso já era algo que eu sabia na teoria, mas depois dessa aula a analogia realmente ficou comigo, e fiquei espantado com a frequência com que depois investigava um problema de encadeamento e encontrava uma fila gigante, ou interrompia a desativação de todos os lugares durante a gravação em uma variável apenas dois encadeamentos utilizados ou mutexes foram mantidos por um longo período em que poderiam ser refatorados para evitá-lo por completo.

Em outras palavras, alguns dos piores problemas de encadeamento são causados ​​por um exagero na tentativa de evitar problemas de encadeamento.


0

Tente fazer isso de novo.

Pelo menos para mim, o que criou uma diferença foi a prática. Depois de fazer o trabalho multiencadeado e distribuído algumas vezes, você pega o jeito.

Eu acho que a depuração é realmente o que dificulta. Posso depurar o código multiencadeado usando o VS, mas estou realmente perdido se precisar usar o gdb. A culpa é minha, provavelmente.

Outra coisa que está aprendendo mais sobre é bloquear estruturas de dados livres.

Eu acho que essa pergunta pode ser realmente melhorada se você especificar a estrutura. Pools de threads .NET e trabalhadores em segundo plano são realmente diferentes do QThread, por exemplo. Sempre há algumas dicas específicas da plataforma.


Estou interessado em ouvir histórias de qualquer estrutura, porque acredito que há coisas a aprender com cada estrutura, especialmente aquelas às quais não fui exposto.
rwong

1
depuradores são em grande parte inúteis em um ambiente multithread.
Pemdas

Eu já tenho rastreadores de execução multiencadeados, o que me diz qual é o problema, mas não vai me ajudar a resolvê-lo. O ponto crucial do meu problema é que "de acordo com o design atual, não posso passar a mensagem X para o objeto Y dessa maneira (sequência); ela deve ser adicionada a uma fila gigante e, eventualmente, será processada; mas por causa disso , não há nenhuma maneira para que as mensagens aparecem para o usuário na hora certa - ele vai sempre acontecer anachronisticly e fazer o usuário muito, muito . confundido Você pode até precisar adicionar barras de progresso, cancelar botões ou mensagens de erro para lugares que shouldn' não tenho esses ".
rwong

0

Aprendi que retornos de chamada de módulos de nível inferior para módulos de nível superior são um grande mal, porque causam a aquisição de bloqueios em uma ordem oposta.


retornos de chamada não são maus ... o fato de que eles fazem algo diferente de quebra de segmento é provavelmente a raiz do mal. Eu seria altamente suspeito de qualquer retorno de chamada que não apenas enviasse um token para a fila de mensagens.
Pemdas

A solução de um problema de otimização (como minimizar f (x)) geralmente é implementada fornecendo o ponteiro para uma função f (x) para o procedimento de otimização, que "chama de volta" enquanto procura o mínimo. Como você faria isso sem um retorno de chamada?
Quant_dev 9/05/11

1
Não há voto negativo, mas os retornos de chamada não são maus. Ligar para um retorno de chamada enquanto mantém um bloqueio é ruim. Não chame nada dentro de um cadeado quando não souber se ele pode travar ou esperar. Isso não inclui apenas retornos de chamada, mas também funções virtuais, funções de API, funções em outros módulos ("nível superior" ou "nível inferior").
Nikie

@nikie: Se um bloqueio deve ser mantido durante o retorno de chamada, o restante da API precisa ser projetado para ser reentrante (muito forte!) ou o fato de você estar com um bloqueio precisa ser uma parte documentada da API ( lamentável, mas às vezes tudo o que você pode fazer).
Donal Fellows

@Donal Fellows: Se um bloqueio deve ser mantido durante um retorno de chamada, eu diria que você tem uma falha de design. Se realmente não há outra maneira, então sim, por todos os meios documentá-lo! Assim como você documentaria se o retorno de chamada será chamado em um encadeamento em segundo plano. Isso faz parte da interface.
Nikie
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.