Estou criando um mecanismo de jogo 2D simples e quero atualizar e renderizar os sprites em diferentes threads, para aprender como isso é feito.
Preciso sincronizar o thread de atualização e o de renderização. Atualmente, eu uso duas bandeiras atômicas. O fluxo de trabalho se parece com:
Thread 1 -------------------------- Thread 2
Update obj ------------------------ wait for swap
Create queue ---------------------- render the queue
Wait for render ------------------- notify render done
Swap render queues ---------------- notify swap done
Nesta configuração, limito o FPS do thread de renderização ao FPS do thread de atualização. Além disso, eu uso sleep()
para limitar o FPS do thread de renderização e atualização para 60, para que as duas funções de espera não esperem muito tempo.
O problema é:
O uso médio da CPU é de cerca de 0,1%. Às vezes, chega a 25% (em um PC quad core). Isso significa que um encadeamento está aguardando o outro, porque a função de espera é um loop while com uma função de teste e configuração, e um loop while usará todos os recursos da CPU.
Minha primeira pergunta é: existe outra maneira de sincronizar os dois threads? Notei que std::mutex::lock
não use a CPU enquanto espera bloquear um recurso, para que não seja um loop while. Como funciona? Não posso usar std::mutex
porque precisarei travá-los em um segmento e desbloquear em outro.
A outra pergunta é; como o programa é executado sempre a 60 FPS, por que o uso da CPU às vezes aumenta para 25%, o que significa que uma das duas esperas está aguardando muito? (os dois threads são limitados a 60fps, portanto, idealmente, não precisarão de muita sincronização).
Edit: Obrigado por todas as respostas. Primeiro, quero dizer que não inicio um novo thread a cada quadro para renderizar. Eu inicio o loop de atualização e renderização no início. Eu acho que multithreading pode economizar algum tempo: Eu tenho as seguintes funções: FastAlg () e Alg (). Alg () é meu objeto de atualização e objeto de renderização e Fastalg () é minha fila de "envio de renderização para" renderizador "". Em um único encadeamento:
Alg() //update
FastAgl()
Alg() //render
Em dois segmentos:
Alg() //update while Alg() //render last frame
FastAlg()
Portanto, talvez o multithreading economize o mesmo tempo. (na verdade, em um aplicativo matemático simples, onde alg é um algoritmo longo e fastalg é mais rápido)
Sei que dormir não é uma boa ideia, embora nunca tenha tido problemas. Isso vai melhorar?
While(true)
{
If(timer.gettimefromlastcall() >= 1/fps)
Do_update()
}
Mas este será um loop while infinito que usará toda a CPU. Posso usar o sono (um número <15) para limitar o uso? Dessa forma, ele será executado a, por exemplo, 100 fps, e a função de atualização será chamada apenas 60 vezes por segundo.
Para sincronizar os dois threads, usarei waitforsingleobject com createSemaphore, para poder bloquear e desbloquear em threads diferentes (sem usar um loop while), não é?
fonte
Respostas:
Para um mecanismo 2D simples com sprites, uma abordagem de thread único é perfeitamente boa. Mas como você deseja aprender a executar multithreading, deve aprender a fazê-lo corretamente.
Não
sleep
para controlar a taxa de quadros. Nunca. Se alguém lhe disser, acerte-o.Primeiro, nem todos os monitores funcionam a 60Hz. Segundo, dois cronômetros marcando na mesma velocidade, correndo lado a lado, sempre acabam sempre fora de sincronia (solte duas bolas de pingue-pongue em uma mesa da mesma altura e ouça). Terceiro,
sleep
é por design nem preciso nem confiável. A granularidade pode ser tão ruim quanto 15,6ms (na verdade, o padrão no Windows [1] ), e um quadro é de apenas 16,6ms a 60fps, o que deixa apenas 1ms para todo o resto. Além disso, é difícil obter 16,6 para ser um múltiplo de 15,6 ...Além disso,
sleep
é permitido (e às vezes!) Retornar somente após 30, 50 ou 100 ms, ou ainda mais.std::mutex
para notificar outro segmento. Não é para isso que serve.Faz
sleep
[2] . Além disso, um cronômetro recorrente contabiliza o tempo corretamente (incluindo o tempo que passa no meio), enquanto o sono por 16,6 ms (ou 16,6 ms menos o tempo_medido_elapsado) não.std::mutex
para que apenas um encadeamento acesse um recurso por vez ("excluir mutuamente") e siga a semântica estranha destd::condition_variable
.std::condition_variable
para bloquear outro encadeamento até que alguma condição seja verdadeira. A semânticastd::condition_variable
desse mutex extra é reconhecidamente bastante estranha e distorcida (principalmente por razões históricas herdadas dos encadeamentos POSIX), mas uma variável de condição é a primitiva correta a ser usada para o que você deseja.Caso você ache
std::condition_variable
estranho demais para se sentir confortável com isso, você também pode simplesmente usar um evento do Windows (um pouco mais lento) ou, se tiver coragem, criar seu próprio evento simples em torno do NtKeyedEvents (envolve coisas assustadoras de baixo nível). Ao usar o DirectX, você já está vinculado ao Windows de qualquer maneira, portanto, a perda de portabilidade não deve ser demais.[1] Sim, você pode definir a taxa do agendador para 1ms, mas isso é mal visto, pois causa muito mais alternância de contexto e consome muito mais energia (em um mundo em que mais e mais dispositivos são dispositivos móveis). Também não é uma solução, pois ainda não torna o sono mais confiável.
[2] Um timer aumentará a prioridade do thread, o que permitirá interromper outro quantum médio de prioridade igual e ser agendado primeiro, o que é um comportamento quase-RT. É claro que não é verdade RT, mas chega muito perto. Despertar do sono significa apenas que o encadeamento fica pronto para ser agendado em algum momento, sempre que possível.
fonte
Não tenho certeza do que você deseja alcançar limitando o FPS da atualização e renderizando ambos para 60. Se você os limitar ao mesmo valor, poderá colocá-los no mesmo segmento.
O objetivo ao separar Atualização e Renderização em threads diferentes é ter ambos "quase" independentes um do outro, para que a GPU possa render 500 FPS e a lógica de Atualização ainda vá para 60 FPS. Você não obtém um ganho de desempenho muito alto ao fazer isso.
Mas você disse que só queria saber como funciona, e está tudo bem. No C ++, um mutex é um objeto especial usado para bloquear o acesso a determinados recursos para outros threads. Em outras palavras, você usa um mutex para tornar os dados sensíveis acessíveis por apenas um thread por vez. Para fazer isso, é bastante simples:
Fonte: http://en.cppreference.com/w/cpp/thread/mutex
EDIT : Verifique se o seu mutex é da classe ou de todo o arquivo, como no link fornecido, ou cada thread criará seu próprio mutex e você não conseguirá nada.
A primeira thread a bloquear o mutex terá acesso ao código interno. Se um segundo thread tentar chamar a função lock (), ele bloqueará até que o primeiro thread a desbloqueie. Portanto, um mutex é uma função de bloqueio, ao contrário de um loop while. As funções de bloqueio não sobrecarregam a CPU.
fonte
std::lock_guard
ou similar, não.lock()
/.unlock()
. RAII não é apenas para gerenciamento de memória!