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:
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.
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.
fonte
Respostas:
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.
fonte
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.
fonte
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
ReaderWriterLockSlim
classe, 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
delegate
objeto 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.fonte
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:
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:
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.fonte