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:
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 .
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)
Educar sua equipe de desenvolvimento sobre os problemas com código multithread.
O que você faria?