Atormentado por bugs multithread


26

Na minha nova equipe que eu gerencio, a maioria do nosso código é de plataforma, soquete TCP e código de rede http. Tudo em C ++. A maioria originou-se de outros desenvolvedores que deixaram a equipe. Os desenvolvedores atuais da equipe são muito inteligentes, mas principalmente juniores em termos de experiência.

Nosso maior problema: erros de simultaneidade multiencadeados. A maioria das bibliotecas de classes é escrita para ser assíncrona usando algumas classes de conjuntos de encadeamentos. Os métodos nas bibliotecas de classes geralmente enfileiram as tarefas de execução longa no pool de encadeamentos a partir de um encadeamento e, em seguida, os métodos de retorno de chamada dessa classe são chamados em um encadeamento diferente. Como resultado, temos muitos bugs de caso de borda que envolvem suposições incorretas de encadeamento. Isso resulta em erros sutis que vão além de apenas ter seções e bloqueios críticos para se proteger contra problemas de simultaneidade.

O que dificulta ainda mais esses problemas é que as tentativas de correção geralmente são incorretas. Alguns erros que eu observei que a equipe estava tentando (ou dentro do próprio código legado) incluem algo como o seguinte:

Erro comum nº 1 - Corrigindo o problema de simultaneidade apenas bloqueando os dados compartilhados, mas esquecendo o que acontece quando os métodos não são chamados na ordem esperada. Aqui está um exemplo muito simples:

void Foo::OnHttpRequestComplete(statuscode status)
{
    m_pBar->DoSomethingImportant(status);
}

void Foo::Shutdown()
{
    m_pBar->Cleanup();
    delete m_pBar;
    m_pBar=nullptr;
}

Portanto, agora temos um bug no qual o Shutdown pode ser chamado enquanto OnHttpNetworkRequestComplete está ocorrendo. Um testador encontra o erro, captura o despejo de memória e atribui o erro a um desenvolvedor. Por sua vez, ele corrige o bug dessa maneira.

void Foo::OnHttpRequestComplete(statuscode status)
{
    AutoLock lock(m_cs);
    m_pBar->DoSomethingImportant(status);
}

void Foo::Shutdown()
{
    AutoLock lock(m_cs);
    m_pBar->Cleanup();
    delete m_pBar;
    m_pBar=nullptr;
}

A correção acima parece boa até você perceber que há um caso ainda mais sutil. O que acontece se o Shutdown for chamado antes que OnHttpRequestComplete seja chamado de volta? Os exemplos do mundo real que minha equipe tem são ainda mais complexos e os casos extremos são ainda mais difíceis de identificar durante o processo de revisão de código.

Erro comum nº 2 - corrigindo problemas de deadlock saindo cegamente do bloqueio, aguarde o término do outro encadeamento e reinsira o bloqueio - mas sem lidar com o caso de o objeto ter sido atualizado pelo outro encadeamento!

Erro comum nº 3 - Embora os objetos sejam contados como referência, a sequência de desligamento "libera" seu ponteiro. Mas esquece de aguardar o thread que ainda está sendo executado para liberar sua instância. Como tal, os componentes são encerrados de maneira limpa e, em seguida, retornos de chamada espúrios ou atrasados ​​são chamados em um objeto em um estado que não espera mais chamadas.

Existem outros casos extremos, mas a linha inferior é esta:

A programação multithread é simplesmente difícil, mesmo para pessoas inteligentes.

À medida que percebo esses erros, passo um tempo discutindo os erros com cada desenvolvedor para desenvolver uma correção mais apropriada. Mas eu suspeito que eles geralmente estejam confusos sobre como resolver cada problema, devido à enorme quantidade de código legado que a correção "correta" envolverá em tocar.

Iremos enviar em breve e tenho certeza de que os patches que estamos aplicando serão válidos para o próximo lançamento. Depois, teremos algum tempo para melhorar a base de código e refatorar sempre que necessário. Não teremos tempo de reescrever tudo. E a maioria do código não é tão ruim assim. Mas estou procurando refatorar o código para que problemas de encadeamento possam ser completamente evitados.

Uma abordagem que estou considerando é essa. Para cada recurso significativo da plataforma, tenha um único thread dedicado no qual todos os eventos e retornos de chamada de rede sejam reunidos. Semelhante ao encadeamento de apartamentos COM no Windows com o uso de um loop de mensagens. Operações de bloqueio longas ainda podem ser despachadas para um encadeamento do pool de trabalho, mas o retorno de chamada de conclusão é invocado no encadeamento do componente. Os componentes podem até compartilhar o mesmo encadeamento. Todas as bibliotecas de classes em execução dentro do encadeamento podem ser escritas sob a suposição de um único mundo encadeado.

Antes de seguir esse caminho, também estou muito interessado em saber se existem outras técnicas ou padrões de design padrão para lidar com problemas com vários segmentos. E eu tenho que enfatizar - algo além de um livro que descreve o básico de mutexes e semáforos. O que você acha?

Também estou interessado em outras abordagens a serem adotadas no processo de refatoração. Incluindo qualquer um dos seguintes:

  1. Literatura ou trabalhos sobre padrões de design em torno de tópicos. Algo além de uma introdução a mutexes e semáforos. Também não precisamos de paralelismo maciço, apenas maneiras de projetar um modelo de objeto para manipular eventos assíncronos de outros threads corretamente .

  2. Maneiras de diagramar o encadeamento de vários componentes, para que seja fácil estudar e desenvolver soluções para. (Ou seja, um equivalente UML para discutir threads entre objetos e classes)

  3. Educar sua equipe de desenvolvimento sobre os problemas com código multithread.

  4. O que você faria?


23
Algumas pessoas, quando confrontadas com um problema, acham que usarei multi-threading. Agora eles têm sondas twolm
Tom Squires

20
o que funciona bem para mim é livrar-se da mutabilidade sempre que possível. Quando vejo o objeto mutável alterando o estado para passar o novo valor, tento refatorá-lo para passar um novo objeto imutável que mantém o valor alterado. Se a inicialização objeto é feito com segurança, este garante a ausência de corridas de dados - um alívio
mosquito

3
Bem-vindo ao inferno multi-thread. Escrevo programas multi-thread / paralell há> 20 anos, em Ada, Occam, C ++. Nunca é fácil, tudo requer um pensamento muito cuidadoso, e quem diz "é fácil simplesmente fazer X" é um tolo que realmente não entende o que está acontecendo. Boa sorte.
Rapidlyow 25/05

2
Se você quiser ver a simultaneidade bem feita, use Erlang! Na verdade, o que você quer é uma forma de modelo de ator de nada compartilhado, onde os casos estranhos de esquina serão eliminados.
Zachary K

3
@DeadMG Eu diria que a simultaneidade de estado compartilhado é inerentemente propensa a casos de canto estranhos e deve ser evitada. Mas Hey eu escrevi um livro sobre Erlang
Zachary K

Respostas:


27

Seu código tem outros problemas significativos , além disso. Excluindo manualmente um ponteiro? Chamando uma cleanupfunção? Owch. Além disso, conforme indicado com precisão no comentário da pergunta, você não usa o RAII para seu bloqueio, que é outra falha bastante épica e garante que, quando DoSomethingImportantlançada uma exceção, coisas terríveis acontecem.

O fato de esse bug multithread estar ocorrendo é apenas um sintoma do problema principal - seu código tem uma semântica extremamente ruim em qualquer situação de segmentação e você está usando ferramentas e ex-idiomas completamente não confiáveis. Se eu fosse você, ficaria surpreso que ele funcione com um único segmento, e muito mais.

Erro comum nº 3 - Embora os objetos sejam contados como referência, a sequência de desligamento "libera" seu ponteiro. Mas esquece de aguardar o thread que ainda está sendo executado para liberar sua instância. Como tal, os componentes são encerrados de maneira limpa e, em seguida, retornos de chamada espúrios ou atrasados ​​são chamados em um objeto em um estado que não espera mais chamadas.

O ponto principal da contagem de referência é que o thread já lançou sua instância . Porque, se não, não poderá ser destruído porque o encadeamento ainda tem uma referência.

Use std::shared_ptr. Quando os tópicos ter lançado (e ninguém , portanto, pode ser chamado de função, como eles não têm ponteiro para ele), em seguida, o destruidor é chamado. Isso é garantido com segurança.

Em segundo lugar, use uma biblioteca de encadeamento real, como os Thread Building Blocks da Intel ou a Parallel Patterns Library da Microsoft. Escrever o seu próprio é demorado e pouco confiável e seu código está cheio de detalhes de segmentação dos quais não precisa. Fazer seus próprios bloqueios é tão ruim quanto fazer seu próprio gerenciamento de memória. Eles já implementaram muitos idiomas de threading muito úteis de uso geral que funcionam corretamente para seu uso.


Essa é uma resposta aceitável, mas não a direção que eu estava procurando, porque gasta muito tempo avaliando um pedaço de código de exemplo que foi escrito apenas por simplicidade (e não reflete o código real em nosso produto). Mas estou curioso sobre um comentário que você fez - "ferramentas não confiáveis". O que é uma ferramenta não confiável? Quais ferramentas você recomenda?
Koncurrency

5
@koncurrency: Uma ferramenta não confiável é algo como gerenciamento manual de memória ou escrever sua própria sincronização, onde, em teoria, resolve um problema X, mas na realidade é tão ruim que você pode garantir erros gigantes e a única maneira de solucionar o problema disponível em uma escala razoável é pelo investimento maciço e desproporcional do tempo do desenvolvedor - que é o que você tem agora.
DeadMG 25/05

9

Outros pôsteres comentaram bem o que deve ser feito para corrigir os problemas principais. Este post está relacionado ao problema mais imediato de corrigir o código herdado, o suficiente para ganhar tempo para refazer tudo da maneira certa. Em outras palavras, essa não é a maneira certa de fazer as coisas, é apenas uma maneira de mancar por enquanto.

Sua idéia de consolidar eventos importantes é um bom começo. Eu chegaria ao ponto de usar um único thread de despacho para lidar com todos os principais eventos de sincronização, sempre que houver dependência de pedidos. Configure uma fila de mensagens seguras para encadeamento e onde quer que você execute operações sensíveis à simultaneidade (alocações, limpezas, retornos de chamada etc.), em vez disso, envie uma mensagem para esse encadeamento e peça para executar ou acionar a operação. A idéia é que esse thread controla todas as partidas, paradas, alocações e limpezas da unidade de trabalho.

O encadeamento de expedição não resolve os problemas que você descreveu, apenas os consolida em um único local. Você ainda precisa se preocupar com eventos / mensagens que ocorrem em ordem inesperada. Eventos com tempos de execução significativos ainda precisarão ser enviados para outros encadeamentos, para que ainda haja problemas de simultaneidade nos dados compartilhados. Uma maneira de atenuar isso é evitar a transmissão de dados por referência. Sempre que possível, os dados nas mensagens de despacho devem ser cópias pertencentes ao destinatário. (Isso é como tornar os dados imutáveis ​​que outros já mencionaram.)

A vantagem dessa abordagem de despacho é que, dentro do encadeamento de despacho, você tem um tipo de porto seguro onde pelo menos sabe que certas operações estão ocorrendo sequencialmente. A desvantagem é que ele cria um gargalo e sobrecarga de CPU extra. Sugiro não se preocupar com nenhuma dessas coisas no início: concentre-se em obter certa medida da operação correta primeiro, movendo o máximo possível para o segmento de despacho. Em seguida, faça alguns perfis para ver o que está consumindo mais tempo de CPU e comece a retirá-lo do encadeamento de expedição usando técnicas corretas de multithreading.

Novamente, o que estou descrevendo não é o caminho certo para fazer as coisas, mas é um processo que pode levá-lo ao caminho certo em incrementos pequenos o suficiente para cumprir os prazos comerciais.


+1 para uma sugestão razoável e intermediária de como superar o desafio existente.

Sim, esta é a abordagem que estou investigando. Você levanta bons pontos sobre desempenho.
koncurrency

Alterar as coisas para passar por um único encadeamento de despacho não parece uma correção rápida, mas um refator maciço para mim.
Sebastian Redl 06/04

8

Com base no código mostrado, você tem uma pilha de WTF. É extremamente difícil, se não impossível, corrigir gradualmente um aplicativo multithread mal escrito. Diga aos proprietários que o aplicativo nunca será confiável sem retrabalho significativo. Faça uma estimativa com base na inspeção e na reformulação de cada parte do código que está interagindo com objetos compartilhados. Primeiro, faça uma estimativa para a inspeção. Então você pode dar uma estimativa para o retrabalho.

Ao refazer o código, planeje escrevê-lo para que ele fique comprovadamente correto. Se você não sabe como fazer isso, encontre alguém que saiba, ou você terminará no mesmo lugar.


Basta ler isso agora, depois que minha resposta foi votada. Só queria dizer que eu amo a frase introdutória :)
back2dos

7

Se você tiver algum tempo para se dedicar à refatoração de seu aplicativo, recomendamos que você dê uma olhada no modelo de ator (consulte, por exemplo , implementações de Theron , Casablanca , libcppa , CAF para C ++).

Atores são objetos que são executados simultaneamente e se comunicam apenas usando troca de mensagens assíncrona. Portanto, todos os problemas de gerenciamento de encadeamentos, mutexes, deadlocks etc. são tratados por uma biblioteca de implementação de atores e você pode se concentrar na implementação do comportamento de seus objetos (atores), o que se resume a repetir o loop

  1. Receber mensagem
  2. Executar computação
  3. Enviar mensagem (s) / criar / matar outros atores.

Uma abordagem para você pode ser ler primeiro o tópico e, possivelmente, dar uma olhada em uma ou duas bibliotecas para ver se o modelo do ator pode ser integrado ao seu código.

Estou usando (uma versão simplificada) deste modelo em um projeto meu há alguns meses e estou impressionado com a robustez dele.


1
A biblioteca Akka para Scala é uma boa implementação disso, que pensa muito em como matar os atores pais quando as crianças morrem, ou vice-versa. Eu sei que não é C ++, mas vale a pena dar uma olhada: akka.io
GlenPeterson 12/12

1
@GlenPeterson: Obrigado, eu sei sobre o akka (que considero a solução mais interessante no momento e funciona tanto com Java quanto com Scala), mas a pergunta aborda C ++ especificamente. Caso contrário, pode-se considerar Erlang. Acho que em Erlang todas as dores de cabeça da programação multi-threading se foram para sempre. Mas talvez estruturas como akka cheguem muito perto.
Giorgio

"Acho que em Erlang todas as dores de cabeça da programação multi-threading se foram para sempre". Eu acho que talvez isso seja um pouco exagerado. Ou, se for verdade, o desempenho pode estar faltando. Eu sei que o Akka não funciona com C ++, apenas dizendo que parece ser o estado da arte para gerenciar vários threads. No entanto, não é seguro para threads. Você ainda pode passar um estado mutável entre os atores e dar um tiro no pé.
GlenPeterson

Eu não sou um especialista em Erlang, mas AFAIK cada ator é executado isoladamente e mensagens imutáveis ​​são trocadas. Portanto, você realmente não precisa lidar com threads e estado mutável compartilhado. O desempenho é provavelmente menor que o C ++, mas isso sempre acontece quando você aumenta o nível de abstração (você aumenta o tempo de execução, mas reduz o tempo de desenvolvimento).
Giorgio

O downvoter pode deixar um comentário e sugerir como posso melhorar esta resposta?
Giorgio

6

Erro comum nº 1 - Corrigindo o problema de simultaneidade apenas bloqueando os dados compartilhados, mas esquecendo o que acontece quando os métodos não são chamados na ordem esperada. Aqui está um exemplo muito simples:

O erro aqui não é o "esquecimento", mas o "não consertar". Se você tem coisas acontecendo em uma ordem inesperada, você tem um problema. Você deve resolvê-lo em vez de tentar contorná-lo (colocar uma trava em algo geralmente é uma solução alternativa).

Você deve tentar adaptar o modelo / sistema de mensagens do ator até um certo grau e ter uma separação de preocupações. O papel de Fooé claramente lidar com algum tipo de comunicação HTTP. Se você deseja projetar seu sistema para fazer isso em paralelo, é a camada acima que deve lidar com os ciclos de vida do objeto e acessar a sincronização de acordo.

É difícil tentar fazer com que vários threads operem nos mesmos dados mutáveis. Mas também raramente é necessário. Todos os casos comuns que exigem isso já foram abstraídos para conceitos mais gerenciáveis ​​e implementados várias vezes para qualquer linguagem imperativa importante. Você apenas tem que usá-los.


2

Seus problemas são muito ruins, mas típicos do mau uso do C ++. A revisão de código corrigirá alguns desses problemas. 30 minutos, um conjunto de globos oculares gera 90% dos resultados. (Citação para isso é googleable)

Problema nº 1 Você precisa garantir que exista uma hierarquia de bloqueio estrita para impedir seu bloqueio.

Se você substituir o Autolock por um invólucro e uma macro, poderá fazer isso.

Mantenha um mapa global estático de bloqueios criados na parte traseira do seu invólucro. Você usa uma macro para inserir as informações de nome de linha e número de linha no construtor Autolock wrapper.

Você também precisará de um gráfico dominador estático.

Agora, dentro da trava, é necessário atualizar o gráfico do dominador e, se você receber uma alteração de pedido, declara um erro e aborta.

Após testes extensivos, você pode se livrar da maioria dos impasses latentes.

O código é deixado como um exercício para o aluno.

O problema 2 desaparecerá (principalmente)

Sua solução arquivual está funcionando. Eu já usei isso antes em sistemas de missão e vida. Minha opinião é essa

  • Passe objetos imutáveis ​​ou faça cópias deles antes de passar.
  • Não compartilhe dados por meio de variáveis ​​públicas ou getters.

  • Eventos externos são enviados por meio de um despacho multithread para uma fila atendida por um encadeamento. Agora você pode classificar o motivo da manipulação de eventos.

  • As alterações de dados que cruzam os encadeamentos entram em um qeuue seguro de encadeamento, são manipuladas por um encadeamento. Faça assinaturas. Agora você pode classificar os motivos dos fluxos de dados.

  • Se seus dados precisarem passar pela cidade, publique-os na fila de dados. Isso irá copiá-lo e passá-lo aos assinantes de forma assíncrona. Também quebra todas as dependências de dados no programa.

Este é praticamente um modelo de ator barato. Os links de Giorgio ajudarão.

Finalmente, seu problema com objetos de desligamento.

Quando você está contando referências, resolveu 50%. Os outros 50% devem fazer referência a contagens de retorno. Passe titulares de retorno de chamada uma referência. A chamada de desligamento deve aguardar a contagem zero na refcount. Não resolve gráficos de objetos complicados; que está entrando na coleta de lixo real. (Qual é a motivação do Java para não fazer promessas sobre quando ou se finalize () será chamado; para tirar você da programação dessa maneira.)


2

Para futuros exploradores: para complementar a resposta sobre o modelo de ator, eu gostaria de adicionar o CSP ( processos sequenciais de comunicação ), com um aceno para a família maior de cálculos de processos em que ele está. O CSP é semelhante ao modelo de ator, mas se divide de maneira diferente. Você ainda tem um monte de threads, mas eles se comunicam através de canais específicos, e não especificamente entre si, e ambos os processos devem estar prontos para enviar e receber respectivamente antes que aconteça. Há também uma linguagem formalizada para provar o código CSP correto. Ainda estou migrando para o uso intenso do CSP, mas já o uso em alguns projetos há alguns meses, e isso é bastante simplificado.

A Universidade de Kent tem uma implementação em C ++ ( https://www.cs.kent.ac.uk/projects/ofa/c++csp/ , clonada em https://github.com/themasterchef/cppcsp2 ).


1

Literatura ou trabalhos sobre padrões de design em torno de tópicos. Algo além de uma introdução a mutexes e semáforos. Também não precisamos de paralelismo maciço, apenas maneiras de projetar um modelo de objeto para manipular eventos assíncronos de outros threads corretamente.

Atualmente, estou lendo isso e explica todos os problemas que você pode obter e como evitá-los, em C ++ (usando a nova biblioteca de encadeamentos, mas acho que as explicações globais são válidas para o seu caso): http: //www.amazon. com / C-Concurrency-Action-Practical-Multithreading / dp / 1933988770 / ref = sr_1_1? ie = UTF8 & qid = 1337934534 & sr = 8-1

Maneiras de diagramar o encadeamento de vários componentes, para que seja fácil estudar e desenvolver soluções para. (Ou seja, um equivalente UML para discutir threads entre objetos e classes)

Pessoalmente, uso uma UML simplificada e apenas assumo que as mensagens são feitas de forma assíncrona. Além disso, isso é verdade entre os "módulos", mas dentro dos módulos eu não quero saber.

Educar sua equipe de desenvolvimento sobre os problemas com código multithread.

O livro ajudaria, mas acho que exercícios / prototipagem e mentor experiente seriam melhores.

O que você faria?

Eu evitaria totalmente que as pessoas que não entendiam os problemas de concorrência trabalhassem no projeto. Mas acho que você não pode fazer isso. Portanto, no seu caso específico, além de tentar garantir que a equipe seja mais educada, não faço ideia.


Obrigado pela sugestão do livro. Eu provavelmente vou buscá-lo.
Koncurrency

Rosquear é realmente difícil. Nem todo programador está à altura do desafio. No mundo dos negócios, toda vez que eu via threads sendo usados, eles eram cercados por bloqueios de forma que não houvesse dois threads em execução ao mesmo tempo. Existem regras que você pode seguir para facilitar, mas ainda é difícil.
GlenPeterson

@GlenPeterson Concordou, agora que tenho mais experiência (desde esta resposta), acho que precisamos de abstrações melhores para torná-lo gerenciável e desencorajar o compartilhamento de dados. Felizmente, os designers de idiomas parecem trabalhar duro nisso.
Klaim #

Fiquei realmente impressionado com o Scala, especificamente por trazer benefícios de imutabilidade de programação funcional, efeitos colaterais mínimos para Java, que é um descendente direto do C ++. Ele é executado na Java Virtual Machine, portanto, pode não ter o desempenho necessário. O livro de Joshua Bloch, "Java Efetivo", trata de minimizar a mutabilidade, criar interfaces herméticas e segurança de threads. Embora seja baseado em Java, aposto que você pode aplicar 80 a 90% dele em C ++. Questionar a mutabilidade e o estado compartilhado (ou a mutabilidade do estado compartilhado) em suas revisões de código pode ser um bom primeiro passo para você.
GlenPeterson

1

Você já está a caminho, reconhecendo o problema e procurando ativamente uma solução. Aqui está o que eu faria:

  • Sente-se e crie um modelo de encadeamento para seu aplicativo. Este é um documento que responde perguntas como: Que tipos de threads você possui? Que coisas devem ser feitas em qual segmento? Quais tipos diferentes de padrões de sincronização você deve usar? Em outras palavras, ele deve descrever as "regras de envolvimento" ao combater problemas de multithreading.
  • Use ferramentas de análise de threads para verificar se há erros na sua base de código. O Valgrind tem um verificador de threads chamado Helgrind, que é bom em detectar coisas como estado compartilhado sendo manipuladas sem sincronização adequada. Certamente existem outras boas ferramentas por aí, vá procurá-las.
  • Considere migrar para longe do C ++. C ++ é um pesadelo para escrever programas concorrentes. Minha escolha pessoal seria Erlang , mas isso é uma questão de gosto.

8
Definitivamente -1 para o último bit. Parece que o código do OP está usando as ferramentas mais primitivas e não as ferramentas C ++ reais.
DeadMG 25/05

2
Eu não concordo A simultaneidade no C ++ é um pesadelo, mesmo se você usar os mecanismos e ferramentas adequados do C ++. E observe que eu escolhi a expressão " considerar ". Entendo perfeitamente que pode não ser uma alternativa realista, mas permanecer com C ++ sem considerar as alternativas é simplesmente bobo.
precisa saber é o seguinte

4
@ JesperE - desculpe, mas não. A simultaneidade no C ++ é apenas um pesadelo se você o fizer em um nível muito baixo. Use uma abstração de encadeamento adequada e não é pior que qualquer outro idioma ou tempo de execução. E com uma estrutura de aplicativo adequada, é realmente tão fácil quanto qualquer outra coisa que eu já vi.
Michael Kohne 25/05

2
Onde trabalho, acredito que temos uma estrutura de aplicativo adequada, usamos as abstrações de encadeamento corretas e assim por diante. Apesar disso, passamos inúmeras horas ao longo dos anos depurando bugs que simplesmente não apareceriam em idiomas projetados adequadamente para simultaneidade. Mas tenho a sensação de que teremos que concordar em discordar disso.
precisa

1
@Espere: Eu concordo com você. O modelo Erlang (para o qual existem implementações para Scala / Java, Ruby e, até onde eu sei, também para C ++) é muito mais robusto do que codificar diretamente com threads.
Giorgio

1

Olhando para o seu exemplo: Assim que o Foo :: Shutdown começar a executar, não será possível chamar OnHttpRequestComplete para executar mais. Isso não tem nada a ver com nenhuma implementação, simplesmente não funciona.

Você também pode argumentar que Foo :: Shutdown não deve ser chamado enquanto uma chamada para OnHttpRequestComplete estiver em execução (definitivamente verdadeira) e provavelmente não se uma chamada para OnHttpRequestComplete ainda estiver pendente.

A primeira coisa a se acertar não é trancar etc., mas a lógica do que é permitido ou não. Um modelo simples seria que sua classe possa ter zero ou mais solicitações incompletas, zero ou mais conclusões que ainda não foram chamadas, zero ou mais conclusões em execução e que seu objeto deseja desligar ou não.

Espera-se que Foo :: Shutdown conclua a execução de execuções, execute solicitações incompletas até o ponto em que elas podem ser encerradas, se possível, para não permitir que mais conclusões sejam iniciadas, para não permitir que mais solicitações sejam iniciadas.

O que você precisa fazer: Adicione especificações às suas funções dizendo exatamente o que elas farão. (Por exemplo, o início de uma solicitação http pode falhar após o desligamento ter sido chamado). E, em seguida, escreva suas funções para que elas atendam às especificações.

Os bloqueios são melhor utilizados apenas pelo menor tempo possível para controlar a modificação de variáveis ​​compartilhadas. Portanto, você pode ter uma variável "performShutDown" protegida por um bloqueio.


0

O que você faria?

Para ser honesto; Eu fugia rapidamente.

Problemas de simultaneidade são desagradáveis . Algo pode funcionar perfeitamente por meses e então (devido ao tempo específico de várias coisas) explode repentinamente na cara do cliente, sem maneira de descobrir o que aconteceu, sem esperança de ver um bom relatório de erro (reproduzível) e sem chance para ter certeza de que não foi uma falha de hardware que não tem nada a ver com o software.

Evitar problemas de simultaneidade precisa começar durante a fase de design, começando exatamente como você fará isso ("ordem de bloqueio global", modelo de ator, ...). Não é algo que você tenta consertar em pânico, na esperança de que tudo não se autodestrua após um próximo lançamento.

Note que não estou brincando aqui. Suas próprias palavras (" A maioria delas se originou de outros desenvolvedores que deixaram a equipe. Os desenvolvedores atuais da equipe são muito inteligentes, mas principalmente iniciantes em termos de experiência. ") Indicam que todas as pessoas com experiência já fizeram o que eu estou sugerindo.

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.