Falando no contexto de um jogo baseado no renderizador openGL:
Vamos supor que existem dois threads:
Atualiza a lógica e a física do jogo, etc. para os objetos do jogo
Faz chamadas de openGL para cada objeto de jogo com base nos dados nos objetos de jogo (esse segmento 1 continua atualizando)
A menos que você tenha duas cópias de cada objeto de jogo no estado atual do jogo, terá que pausar o Tópico 1 enquanto o Tópico 2 faz as chamadas de empate, caso contrário, os objetos do jogo serão atualizados no meio de uma chamada de empate para esse objeto, o que é indesejável!
Mas a interrupção do encadeamento 1 para fazer chamadas de desenho com segurança do encadeamento 2 mata todo o propósito de multithreading / simultaneidade
Existe uma abordagem melhor para isso além de usar centenas ou milhares ou sincronizar objetos / cercas para que a arquitetura multicore possa ser explorada para desempenho?
Sei que ainda posso usar o multithreading para carregar textura e compilar shaders para os objetos que ainda farão parte do estado atual do jogo, mas como faço para os objetos ativos / visíveis sem causar conflito com o desenho e a atualização?
E se eu usar um bloqueio de sincronização separado em cada objeto do jogo? Dessa forma, qualquer encadeamento bloquearia apenas um objeto (caso ideal) e não durante todo o ciclo de atualização / desenho! Mas qual o custo das fechaduras em cada objeto (o jogo pode ter mil objetos)?
fonte
Respostas:
A abordagem que você descreveu, usando bloqueios, seria muito ineficiente e provavelmente mais lenta do que usar um único encadeamento. A outra abordagem de manter cópias de dados em cada encadeamento provavelmente funcionaria bem "em termos de velocidade", mas com um custo proibitivo de memória e complexidade de código para manter as cópias sincronizadas.
Existem várias abordagens alternativas para isso, uma solução popular para renderização multithread é usando um buffer duplo de comandos. Isso consiste em executar o backend do renderizador em um encadeamento separado, onde todas as chamadas de empate e comunicação com a API de renderização são realizadas. O thread de front-end que executa a lógica do jogo se comunica com o renderizador de back-end por meio de um buffer de comando (com buffer duplo). Com essa configuração, você tem apenas um ponto de sincronização na conclusão de um quadro. Enquanto o front-end está preenchendo um buffer com comandos de renderização, o back-end está consumindo o outro. Se os dois fios estiverem bem equilibrados, nenhum deve passar fome. Essa abordagem é subótima, no entanto, uma vez que introduz latência nos quadros renderizados, além disso, é provável que o driver OpenGL já esteja fazendo isso em seu próprio processo, portanto, os ganhos de desempenho precisam ser cuidadosamente medidos. Ele também usa apenas dois núcleos, na melhor das hipóteses. Essa abordagem foi usada em vários jogos de sucesso, como Doom 3 e Quake 3
As abordagens mais escaláveis que utilizam melhor as CPUs com vários núcleos são baseadas em tarefas independentes , nas quais você dispara uma solicitação assíncrona que é atendida em um encadeamento secundário, enquanto o encadeamento que disparou a solicitação continua com algum outro trabalho. Idealmente, a tarefa não deve ter dependências com os outros encadeamentos, para evitar bloqueios (também evite dados compartilhados / globais como a praga!). As arquiteturas baseadas em tarefas são mais utilizáveis em partes localizadas de um jogo, como animações computacionais, busca de caminhos de IA, geração de procedimentos, carregamento dinâmico de objetos de cena etc. Os jogos são naturalmente cheios de eventos, a maioria dos tipos de eventos é assíncrona. é fácil fazê-los rodar em threads separados.
Por fim, recomendo a leitura:
Fundamentos de segmentação para jogos da Intel.
Concorrência efetiva de Herb Sutter (vários links para outros bons recursos nesta página).
fonte