Como posso tornar a passagem de mensagens entre threads em um mecanismo multithread menos complicada?

18

O mecanismo C ++ em que estou trabalhando atualmente está dividido em vários segmentos grandes - geração (para criar meu conteúdo processual), jogabilidade (para IA, scripts, simulação), física e renderização.

Os encadeamentos se comunicam por meio de pequenos objetos de mensagem, que passam de um encadeamento para outro. Antes de pisar, um encadeamento processa todas as suas mensagens recebidas - atualizações para transformações, adição e remoção de objetos, etc. Às vezes, um encadeamento (Geração) cria algo (Art) e o passa para outro encadeamento (Rendering) para propriedade permanente.

No início do processo, notei algumas coisas:

  1. O sistema de mensagens é complicado. Criar um novo tipo de mensagem significa subclassificar a classe Message base, criar uma nova enumeração para seu tipo e escrever uma lógica de como os encadeamentos devem interpretar o novo tipo de mensagem. É um salto de velocidade para o desenvolvimento e é propenso a erros de digitação. (Sidenote - trabalhar nisso me faz apreciar como as grandes linguagens dinâmicas podem ser!)

    Existe uma maneira melhor de fazer isso? Devo usar algo como boost :: bind para tornar isso automático? Estou preocupado que, se fizer isso, perco a capacidade de dizer, classifique as mensagens com base no tipo ou algo assim. Não tenho certeza se esse tipo de gerenciamento será necessário.

  2. O primeiro ponto é importante porque esses threads fazem muita comunicação. Criar e transmitir mensagens é uma grande parte de fazer as coisas acontecerem. Gostaria de otimizar esse sistema, mas também estar aberto a outros paradigmas que podem ser igualmente úteis. Existem projetos multithread diferentes em que devo pensar para ajudar a tornar isso mais fácil?

    Por exemplo, existem alguns recursos que são escritos com pouca frequência, mas lidos frequentemente a partir de vários encadeamentos. Devo estar aberto à idéia de ter dados compartilhados, protegidos por mutexes, que todos os threads possam acessar?

Esta é minha primeira vez projetando algo com multithreading em mente desde o início. Nesse estágio inicial, acho que está indo muito bem (considerando), mas estou preocupado com o dimensionamento e com minha própria eficiência na implementação de coisas novas.

Raptormeat
fonte
6
Realmente não há uma pergunta única e direcionada aqui, e, como tal, este post não se encaixa no estilo de perguntas e respostas deste site. Eu recomendo que você divida sua postagem em postagens distintas, uma por pergunta, e refocalize as perguntas para que elas perguntem sobre um problema específico que você está realmente enfrentando, em vez de uma vaga coleção de dicas ou conselhos.
2
Se você quiser se envolver em uma conversa mais geral, recomendo que você experimente este post nos fóruns em gamedev.net . Como Josh disse, como sua "pergunta" não é uma pergunta específica específica, seria muito difícil acomodar no formato StackExchange.
Cypher
Obrigado pelo feedback pessoal! Eu esperava que alguém com mais conhecimento tivesse um único recurso / experiência / paradigma que pudesse resolver vários dos meus problemas ao mesmo tempo. Estou sentindo que uma grande idéia poderia sintetizar meus vários problemas em uma coisa que estou perdendo, e eu estava pensando que alguém com mais experiência do que eu poderia reconhecer isso ... Mas talvez não, e de qualquer maneira - pontos !
precisa saber é o seguinte
Renomeei o seu título para ser mais específico à passagem de mensagens, pois as perguntas do tipo "dicas" implicam que não há um problema específico a ser resolvido (e, portanto, hoje em dia eu fechava como "não é uma pergunta real").
Tetrad
Tem certeza de que precisa de tópicos separados para física e jogabilidade? Esses dois parecem estar muito entrelaçados. Além disso, é difícil saber como oferecer conselhos sem saber como cada um deles se comunica e com quem.
Nicol Bolas

Respostas:

10

Para o seu problema mais amplo, tente encontrar maneiras de reduzir ao máximo a comunicação entre threads. É melhor evitar completamente os problemas de sincronização, se você puder. Isso pode ser alcançado com o buffer duplo de seus dados, introduzindo uma latência de atualização única, mas facilitando bastante a tarefa de trabalhar com dados compartilhados.

Como um aparte, você considerou não encadear pelo subsistema, mas usar a geração de encadeamentos ou conjuntos de encadeamentos para dividir por tarefa? (veja isso em relação ao seu problema específico, para o pool de threads.) Este breve documento descreve de maneira concisa o objetivo e o uso do padrão de pool. Veja estas respostas informativasAlém disso. Conforme observado lá, os pools de threads melhoram a escalabilidade como um bônus. E é "escreva uma vez, use em qualquer lugar", em vez de ter que fazer com que os threads baseados no subsistema tenham um bom desempenho cada vez que você escreve um novo jogo ou mecanismo. Também existem muitas soluções sólidas de pool de threads de terceiros. Seria mais simples começar com a geração de encadeamentos e trabalhar para agrupamentos de encadeamentos mais tarde, se a sobrecarga de encadeamentos de desova e destruição precisasse ser atenuada.

Engenheiro
fonte
11
Alguma recomendação para consultar bibliotecas específicas de pool de threads?
Imre
Nick- muito obrigado pela resposta. Quanto ao seu primeiro ponto, acho que é uma ótima idéia e provavelmente a direção em que irei seguir. No momento, é cedo o suficiente para que eu ainda não saiba o que precisaria ser armazenado em buffer duplo. Vou manter isso em mente, pois isso se solidifica com o tempo. Para o seu segundo ponto, obrigado pela sugestão! Sim, os benefícios das tarefas de encadeamento são claros. Vou ler seus links e pensar sobre isso. Não tenho 100% de certeza se vai funcionar para mim / como fazê-lo funcionar para mim, mas definitivamente vou pensar seriamente. Obrigado!
Raptormeat
11
@imre, verifique a biblioteca Boost - eles têm futuros, que são uma maneira agradável / fácil de abordar essas coisas.
Jonathan Dickinson
5

Você perguntou sobre diferentes designs com vários segmentos. Um amigo meu me contou sobre esse método que achei muito legal.

A idéia é que haveria 2 cópias de cada entidade do jogo (desperdício, eu sei). Uma cópia seria a cópia atual e a outra seria a cópia anterior. A cópia atual é estritamente somente para gravação e a cópia anterior é estritamente somente para leitura . Ao atualizar, você atribui intervalos da sua lista de entidades a quantos segmentos você achar necessário. Cada encadeamento tem acesso de gravação às cópias presentes no intervalo atribuído e cada encadeamento tem acesso de leitura a todas as cópias anteriores das entidades e, portanto, pode atualizar as cópias presentes atribuídas usando os dados das cópias anteriores sem bloqueio. Entre cada quadro, a cópia atual se torna a cópia anterior, no entanto, você deseja lidar com a troca de funções.

John McDonald
fonte
4

Tivemos o mesmo problema, apenas com C #. Depois de pensar muito sobre a facilidade (ou a falta dela) de criar novas mensagens, o melhor que podíamos fazer era criar um gerador de código para elas. É um pouco feio, mas utilizável: dada apenas uma descrição do conteúdo da mensagem, ele gera classe de mensagem, enumerações, código de manipulação de espaço reservado etc. - todo esse código que é quase o mesmo a cada vez e realmente propenso a erros de digitação.

Não estou totalmente feliz com isso, mas é melhor do que escrever todo esse código manualmente.

No que diz respeito aos dados compartilhados, a melhor resposta é, obviamente, "depende". Mas geralmente, se alguns dados são lidos com frequência e necessários por muitos encadeamentos, o compartilhamento vale a pena. Para segurança do thread, sua melhor aposta é torná-lo imutável , mas se isso estiver fora de questão, o mutex pode ajudar. No C # há uma ReaderWriterLockSlimclasse, projetada especificamente para esses casos; Tenho certeza de que existe um equivalente em C ++.

Uma outra idéia para comunicação de encadeamento, que provavelmente resolve seu primeiro problema, é passar manipuladores em vez de mensagens. Não sei como resolver isso em C ++, mas em C # você pode enviar um delegateobjeto para outro thread (como em, adicioná-lo a algum tipo de fila de mensagens) e realmente chamar esse delegado do thread de recebimento. Isso torna possível criar mensagens "ad hoc" no local. Eu só brinquei com essa idéia, nunca tentei na produção, então pode ser ruim de fato.

deixa pra lá
fonte
Obrigado por todas as ótimas informações! A última parte sobre manipuladores é semelhante ao que eu estava mencionando sobre o uso de binding ou functors para passar funções. Eu meio que gosto da idéia - eu posso experimentá-la e ver se ela é péssima ou impressionante: D Pode começar criando uma classe CallDelegateMessage e mergulhando meu dedo na água.
Raptormeat
1

Estou apenas na fase de design de algum código de jogo encadeado, então só posso compartilhar meus pensamentos, não qualquer experiência real. Com isso dito, estou pensando nas seguintes linhas:

  • A maioria dos dados do jogo deve ser compartilhada para acesso somente leitura .
  • É possível gravar dados usando um tipo de mensagem.
  • Para evitar a atualização de dados enquanto outro thread os lê, o loop do jogo tem duas fases distintas: ler e atualizar.
  • Na fase de leitura:
  • Todos os dados compartilhados são somente leitura para todos os threads.
  • Os encadeamentos podem calcular itens (usando armazenamento local do encadeamento) e produzir solicitações de atualização , que são basicamente objetos de comando / mensagem, colocados em uma fila, para serem aplicados posteriormente.
  • Na fase de atualização:
  • Todos os dados compartilhados são somente gravação. Os dados devem ser assumidos em um estado desconhecido / instável.
  • É aqui que os objetos de solicitação de atualização são processados.

Acho que (embora não tenha certeza) em teoria, isso deve significar que, durante as fases de leitura e atualização, qualquer número de threads pode ser executado simultaneamente com sincronização mínima. Na fase de leitura, ninguém está gravando os dados compartilhados, portanto, nenhum problema de simultaneidade deve surgir. A fase de atualização, bem, é mais complicada. Atualizações paralelas no mesmo dado seriam um problema, portanto, alguma sincronização está em ordem aqui. No entanto, eu ainda poderia executar um número arbitrário de encadeamentos de atualização, desde que estejam operando em conjuntos de dados diferentes.

No total, acho que essa abordagem se prestaria bem a um sistema de pool de threads. As partes problemáticas são:

  • Sincronizando os threads de atualização (verifique se não há vários threads tentando atualizar o mesmo conjunto de dados).
  • Certificando-se de que, na fase de leitura, nenhum thread possa gravar dados compartilhados acidentalmente. Receio que haja muito espaço para erros de programação e não sei quanto deles poderia ser facilmente capturado por ferramentas de depuração.
  • Escreva código de uma maneira que você não possa contar com os resultados intermediários disponíveis para leitura imediata. Ou seja, você não pode escrever x += 2; if (x > 5) ...se x for compartilhado. Você precisa fazer uma cópia local de x ou produzir uma solicitação de atualização e fazer apenas o condicional na próxima execução. O último significaria um monte de código adicional para preservar o estado local do encadeamento.
imre
fonte