Como devo estruturar minhas classes para permitir a simulação multithread?

8

No meu jogo, existem terrenos com edifícios (casas, centros de recursos). Prédios como casas têm inquilinos, quartos, complementos, etc., e há vários valores que precisam ser simulados com base em todas essas variáveis.

Agora, gostaria de usar o AndEngine para o material de front-end e criar outro encadeamento para fazer os cálculos de simulação (talvez também inclua a IA posteriormente nesse encadeamento). Isso é para que um thread inteiro não faça todo o trabalho e cause problemas como bloqueio. Isso introduz o problema de simultaneidade e dependência .

O problema da moeda é o meu thread principal da interface do usuário e o thread de cálculo precisaria acessar todos os objetos de simulação. Portanto, tenho que torná-los seguros para threads, mas não sei como armazenar e estruturar os objetos de simulação para permitir isso.

O problema da dependência é que, para calcular valores, meus cálculos dependem dos valores de outros objetos.

Qual seria a melhor maneira de vincular meu objeto de inquilino no prédio aos meus cálculos? Codifique-o para a classe de inquilino? Qual é uma boa maneira de "armazenar" algoritmos para que eles sejam facilmente ajustados?

Uma maneira simples e preguiçosa seria arrastar tudo para uma classe que contenha todo o objeto, como terrenos (que, por sua vez, mantêm os prédios, etc.). Essa classe também manteria o estado do jogo, como a tecnologia disponível para o usuário, conjuntos de objetos para itens como sprites. Mas essa é uma maneira preguiçosa e perigosa, correto?

Edit: Eu estava olhando para injeção de dependência, mas quão bem isso lida com como uma classe que contém outros objetos? ou seja, meu lote de terreno, com um prédio que possui inquilino e uma série de outros valores. O DI também parece uma dor no bumbum com o AndEngine.

NiffyShibby
fonte
Apenas uma observação rápida, não há preocupação com o acesso simultâneo aos dados se um dos acessos for somente leitura. Contanto que você mantenha sua renderização apenas lendo os dados brutos para usá-los na renderização e não atualizando os dados durante o processamento, não há problema. Uma thread atualiza os dados, a outra thread apenas lê e renderiza.
James
Bem, o acesso simultâneo ainda é um problema, pois o usuário pode comprar um lote de terreno, construir um prédio naquele terreno e colocar um inquilino em uma casa, de modo que o segmento principal está criando dados e pode modificá-los. O acesso simultâneo não é um problema, é mais uma instância de compartilhamento entre o thread principal e o thread filho.
NiffyShibby
Falo da dependência como um problema, parece que pessoas como o Google Guice acham que esconder dependência não é uma coisa sábia. Meus cálculos inquilino dependem do terreno para construção, construção, criando um sprite na tela (eu poderia ter uma relação relacional entre um inquilino edifício e criar um sprite inquilino em outros lugares)
NiffyShibby
Acho que minha sugestão deve ser interpretada como fazendo com que as coisas que são descartadas sejam autocontidas ou exijam acesso somente leitura aos dados gerenciados por outra thread. A renderização seria um exemplo de algo que você poderia descartar como faria só precisa de acesso de leitura aos dados para que eles possam ser exibidos.
James
11
James, mesmo um acesso somente leitura pode ser uma má idéia se outro thread estiver no meio de fazer alterações nesse objeto. Com uma estrutura de dados complexa, isso pode causar uma falha e, com tipos de dados simples, pode causar uma leitura inconsistente.
Kylotan

Respostas:

4

Seu problema é inerentemente serial - você deve concluir uma atualização da simulação antes de poder renderizá-la. Transferir a simulação para um encadeamento diferente significa simplesmente que o encadeamento da interface do usuário principal não faz nada enquanto o encadeamento da simulação está marcado (o que significa que está bloqueado).

A "melhor prática" comumente aceita para simultaneidade não é colocar sua renderização em um thread e sua simulação em outro, como você está propondo. Eu recomendo fortemente contra essa abordagem, de fato. As duas operações são naturalmente relacionadas em série e, embora possam ser brutamente forçadas, não são ideais e não são dimensionáveis .

Uma abordagem melhor é tornar partes da atualização ou renderização simultâneas, mas deixe a atualização e a renderização sempre seriais. Por exemplo, se você tem um limite natural em sua simulação (por exemplo, se casas nunca se afetam na simulação), você pode enfiar todas as casas em baldes de N casas e girar um monte de threads para cada processo bucket e deixe esses threads se unirem antes que a etapa de atualização seja concluída. Isso é muito melhor e se encaixa muito melhor ao design simultâneo.

Você está pensando demais no restante da questão:

A injeção de dependência é um arenque vermelho aqui: tudo o que a injeção de dependência realmente significa é que você passa ("injeta") as dependências de uma interface para instâncias dessa interface, normalmente durante a construção.

Isso significa que, se você tem uma classe que modela a House, que precisa saber coisas sobre a Cityqual está, o Houseconstrutor pode se parecer com:

public House( City containingCity ) {
  m_city = containingCity; // Store in a member variable for later access
  ...
}

Nada especial.

O uso de um singleton é desnecessário (você costuma vê-lo em algumas das "estruturas de DI" extremamente complexas e com engenharia excessiva, como o Caliburn, projetadas para aplicativos GUI "corporativos" - isso não a torna uma boa solução). De fato, a introdução de singletons é frequentemente a antítese do bom gerenciamento de dependências. Eles também podem causar sérios problemas ao código multithread, porque geralmente não podem ser protegidos contra threads sem bloqueios - quanto mais bloqueios você precisar adquirir, pior será o seu problema adequado para lidar com uma natureza paralela.


fonte
Lembro-me de dizer que os singletons eram ruins no meu post original ...
NiffyShibby
Lembro-me de dizer que singletons eram ruins no meu post original, mas isso foi removido. Acho que estou entendendo o que você está dizendo. Por exemplo, minha pequena pessoa está percorrendo uma tela, pois está fazendo com que o thread de atualização seja chamado, ele precisa atualizá-lo, mas não pode porque o thread principal está usando o objeto, portanto meu outro thread está bloqueado. Onde, como eu deveria estar atualizando entre a renderização.
usar o seguinte código
Alguém me enviou um link útil. gamedev.stackexchange.com/questions/95/...
NiffyShibby
5

A solução usual para problemas de simultaneidade é o isolamento de dados .

Isolamento significa que cada encadeamento possui seus próprios dados e não toca nos dados de outros encadeamentos. Dessa forma, não há problemas com simultaneidade ... mas então temos um problema de comunicação. Como esses threads podem trabalhar juntos se não compartilham dados?

Existem duas abordagens aqui.

O primeiro é a imutabilidade . Estruturas / variáveis ​​imutáveis ​​são aquelas que nunca mudam de estado. A princípio, isso pode parecer inútil - como alguém pode usar uma "variável" que nunca muda? No entanto, podemos trocar essas variáveis! Considere este exemplo: suponha que você tenha uma Tenantclasse com vários campos, necessária para estar em um estado consistente. Se você alterar um Tenantobjeto no segmento A e, ao mesmo tempo, observá-lo no segmento B, o segmento B poderá ver o objeto em estado inconsistente. No entanto, se Tenantfor imutável, o segmento A não pode alterá-lo. Em vez disso, cria novas Tenantobjeto com os campos configurados conforme necessário e o troca pelo antigo. Trocar é apenas uma alteração em uma referência, que provavelmente é atômica e, portanto, não há como observar o objeto em estado inconsistente.

A segunda abordagem é a troca de mensagens . A idéia por trás disso é que, quando todos os dados são "de propriedade" de algum thread, podemos dizer a ele o que fazer com os dados. Cada encadeamento nesta arquitetura possui uma fila de mensagens - uma lista de Messageobjetos e uma bomba de mensagens - executando constantemente o método que remove uma mensagem da fila, interpreta e chama algum método manipulador. Por exemplo, suponha que você tenha explorado um terreno, sinalizando que ele precisa ser comprado. O segmento da interface do usuário não pode alterar o Plotobjeto diretamente, porque pertence ao segmento lógico (e provavelmente é imutável). Portanto, o thread da interface do usuário constrói um BuyMessageobjeto e o adiciona à fila do thread lógico. O encadeamento lógico, quando em execução, pega a mensagem da fila e chamaBuyPlot(), extraindo os parâmetros do objeto de mensagem. Pode enviar uma mensagem de volta, por exemplo BuySuccessfulMessage, instruindo o thread da interface do usuário a colocar um "Agora você tem mais terreno!" janela na tela. Obviamente, o acesso à fila de mensagens deve ser sincronizado com o bloqueio, a seção crítica ou o que for chamado no AndEngine. Mas esse é um ponto único de sincronização entre os threads, e os threads são suspensos por um período muito curto, portanto não há problema.

Essas duas abordagens são melhor usadas em combinação. Seus encadeamentos devem se comunicar com as mensagens e ter alguns dados imutáveis ​​"abertos" para outros encadeamentos - por exemplo, uma lista imutável de plotagens para a interface do usuário desenhá-los.

Observe também que "somente leitura" não significa necessariamente imutável ! Qualquer estrutura de dados complexa, como uma hashtable, pode alterar seu estado interno nos acessos de leitura; portanto, verifique primeiro a documentação.

deixa pra lá
fonte
Isso soa muito bem, vou ter que fazer alguns testes com ele, parece muito caro dessa maneira, eu estava pensando nas linhas do DI com um escopo de singleton e, em seguida, usar bloqueios para o acesso simultâneo. Mas eu nunca pensei em fazê-lo desta maneira, poderia funcionar: D
NiffyShibt
Bem, é assim que fazemos simultaneidade em servidor multithread altamente concorrente. Provavelmente um pouco exagero para um jogo simples, mas essa é a abordagem que eu usaria.
Nevermind
4

Provavelmente 99% dos programas de computador escritos no histórico usavam apenas 1 thread e funcionavam bem. Eu não tenho nenhuma experiência com o AndEngine, mas é muito raro encontrar sistemas que exijam encadeamento, apenas vários que poderiam ter se beneficiado com ele, dado o hardware certo.

Tradicionalmente, para fazer simulação e GUI / renderização em um encadeamento, você simplesmente faz um pouco da simulação, depois renderiza e repete, normalmente muitas vezes por segundo.

Quando alguém tem pouca experiência no uso de vários processos ou não entende completamente o que significa 'segurança' do segmento (que é um termo vago que pode significar muitas coisas diferentes), é muito fácil introduzir muitos erros no sistema. Então, pessoalmente, eu recomendaria adotar a abordagem de encadeamento único, intercalar simulação e renderização e salvar qualquer encadeamento para operações que você sabe que levarão muito tempo e exigirão absolutamente encadeamentos e não um modelo baseado em evento.

Kylotan
fonte
O Andengine faz a renderização para mim, mas ainda sinto que os cálculos precisam ser executados em outro thread, pois o thread principal da interface do usuário acabaria se desacelerando se não fosse bloqueado se tudo fosse feito em um thread.
usar o seguinte código
Por que você sente isso? Você tem cálculos mais caros que um jogo 3D típico? E você sabe que a maioria dos dispositivos Android tem apenas 1 núcleo e, portanto, não obtém nenhum benefício intrínseco de desempenho com threads extras?
Kylotan
Não, mas é bom separar a lógica e definir claramente o que está sendo feito. Se você a mantiver no mesmo encadeamento, terá que fazer referência à classe principal onde isso é realizado ou fazer algum DI com escopo singleton. O que não é tanto um problema. Quanto ao núcleo, estamos vendo mais dispositivos Android com núcleo duplo, minha ideia de jogo pode não ter um bom desempenho em um dispositivo de núcleo único, enquanto em um núcleo duplo pode funcionar muito bem.
usar o seguinte código
Portanto, projetar tudo em um thread inteiro não me parece uma ótima idéia, pelo menos com os threads eu posso separar a lógica e, no futuro, não ter que me preocupar em tentar melhorar o desempenho, como eu projetei desde o início.
usar o seguinte código
Mas como o problema ainda é inerentemente serial, você provavelmente bloqueará os dois threads esperando que eles entrem, a menos que você tenha isolado os dados (dando ao thread de renderização algo para fazer enquanto o thread lógico funciona) e renderizando um quadro ou mais por trás da simulação. A abordagem que você está descrevendo não é a melhor prática comumente aceita para design de simultaneidade.