Estoy creando un motor de juego 2D simple y quiero actualizar y renderizar los sprites en diferentes hilos, para aprender cómo se hace.
Necesito sincronizar el hilo de actualización y el de render. Actualmente, uso dos banderas atómicas. El flujo de trabajo se parece a:
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
En esta configuración, limito el FPS del hilo de renderizado al FPS del hilo de actualización. Además, uso sleep()
para limitar tanto el renderizado como la actualización del FPS del hilo a 60, por lo que las dos funciones de espera no esperarán mucho tiempo.
El problema es:
El uso promedio de la CPU es de alrededor del 0.1%. A veces sube hasta un 25% (en una PC de cuatro núcleos). Significa que un subproceso está esperando al otro porque la función de espera es un bucle while con una función de prueba y configuración, y un bucle while utilizará todos los recursos de su CPU.
Mi primera pregunta es: ¿hay otra forma de sincronizar los dos hilos? Noté que std::mutex::lock
no use la CPU mientras está esperando bloquear un recurso, por lo que no es un ciclo while. ¿Como funciona? No puedo usar std::mutex
porque tendré que bloquearlos en un hilo y desbloquearlos en otro hilo.
La otra pregunta es; Dado que el programa se ejecuta siempre a 60 FPS, ¿por qué a veces su uso de CPU salta al 25%, lo que significa que uno de los dos espera mucho? (los dos hilos están limitados a 60 fps, por lo que idealmente no necesitarán mucha sincronización).
Editar: Gracias por todas las respuestas. Primero quiero decir que no comienzo un nuevo hilo en cada cuadro para renderizar. Comienzo tanto la actualización como el bucle de renderizado al principio. Creo que el subprocesamiento múltiple puede ahorrar algo de tiempo: tengo las siguientes funciones: FastAlg () y Alg (). Alg () es mi obj de actualización y obj de render y Fastalg () es mi "cola de envío de render a" renderizador ". En un solo hilo:
Alg() //update
FastAgl()
Alg() //render
En dos hilos:
Alg() //update while Alg() //render last frame
FastAlg()
Entonces, quizás el multiproceso puede ahorrar el mismo tiempo. (en realidad, en una aplicación matemática simple, sí, donde alg es un algoritmo largo y es más rápido)
Sé que dormir no es una buena idea, aunque nunca he tenido problemas. ¿Será esto mejor?
While(true)
{
If(timer.gettimefromlastcall() >= 1/fps)
Do_update()
}
Pero este será un ciclo while infinito que usará toda la CPU. ¿Puedo usar el modo de suspensión (un número <15) para limitar el uso? De esta manera, se ejecutará, por ejemplo, a 100 fps, y la función de actualización se llamará solo 60 veces por segundo.
Para sincronizar los dos hilos, usaré waitforsingleobject con createSemaphore para poder bloquear y desbloquear diferentes hilos (sin usar un bucle while), ¿no?
fuente
Respuestas:
Para un motor 2D simple con sprites, un enfoque de un solo subproceso es perfectamente bueno. Pero dado que desea aprender a hacer subprocesos múltiples, debe aprender a hacerlo correctamente.
No haga
sleep
para controlar la velocidad de fotogramas. Nunca. Si alguien te dice que lo hagas, golpéalo.Primero, no todos los monitores funcionan a 60Hz. En segundo lugar, dos temporizadores que funcionan a la misma velocidad corriendo uno al lado del otro siempre se desincronizarán (suelte dos pelotas de ping-pong en una mesa desde la misma altura y escuche). Tercero, por diseño no
sleep
es ni preciso ni confiable. La granularidad puede ser tan mala como 15,6 ms (de hecho, el valor predeterminado en Windows [1] ), y un marco es de solo 16,6 ms a 60 fps, lo que deja solo 1 ms para todo lo demás. Además, es difícil obtener 16.6 para que sea un múltiplo de 15.6 ... Además, se permite (¡y a veces lo hará!) Regresar solo después de 30 o 50 o 100 ms, o incluso más tiempo.sleep
std::mutex
para notificar a otro hilo. Esto no es para lo que sirve.Hacer
sleep
[2] . Además, un temporizador recurrente representa el tiempo correctamente (incluido el tiempo que transcurre entre ellos) mientras que dormir 16.6 ms (o 16.6 ms menos medido_tiempo_elapsado) no.std::mutex
para que solo un hilo acceda a un recurso a la vez ("excluirse mutuamente") y para cumplir con la extraña semántica destd::condition_variable
.std::condition_variable
para bloquear otro hilo hasta que se cumpla alguna condición. La semántica destd::condition_variable
con ese mutex adicional es ciertamente bastante extraña y retorcida (principalmente por razones históricas heredadas de hilos POSIX), pero una variable de condición es la primitiva correcta para usar para lo que desea.En caso de que te parezca
std::condition_variable
demasiado extraño para sentirte cómodo con él, también puedes simplemente usar un evento de Windows (un poco más lento) o, si eres valiente, construir tu propio evento simple alrededor de NtKeyedEvents (implica cosas aterradoras de bajo nivel). A medida que usa DirectX, ya está vinculado a Windows de todos modos, por lo que la pérdida de portabilidad no debería ser un problema.[1] Sí, puede establecer la velocidad del planificador en 1 ms, pero esto está mal visto ya que causa muchos más cambios de contexto y consume mucha más energía (en un mundo donde cada vez más dispositivos son dispositivos móviles). Tampoco es una solución ya que todavía no hace que el sueño sea más confiable.
[2] Un temporizador aumentará la prioridad del hilo, lo que le permitirá interrumpir otro hilo de igual prioridad a mitad del cuántico y se programará primero, lo cual es un comportamiento cuasi-RT. Por supuesto, no es verdadero RT, pero se acerca mucho. Despertar del sueño simplemente significa que el hilo está listo para ser programado en algún momento, siempre que sea posible.
fuente
No estoy seguro de lo que quiere lograr al limitar el FPS de la actualización y el renderizado a 60. Si los limita al mismo valor, podría haberlos puesto en el mismo hilo.
El objetivo al separar Update y Render en diferentes subprocesos es tener ambos "casi" independientes el uno del otro, de modo que la GPU pueda generar 500 FPS y la lógica de Actualización todavía vaya a 60 FPS. No logras una ganancia de rendimiento muy alta al hacerlo.
Pero dijiste que solo querías saber cómo funciona, y está bien. En C ++, un mutex es un objeto especial que se usa para bloquear el acceso a ciertos recursos para otros subprocesos. En otras palabras, usa un mutex para hacer que los datos sensibles sean accesibles por un solo hilo a la vez. Para hacerlo, es bastante simple:
Fuente: http://en.cppreference.com/w/cpp/thread/mutex
EDITAR : asegúrese de que su mutex sea de clase o de todo el archivo, como en el enlace proporcionado, de lo contrario, cada hilo creará su propio mutex y no logrará nada.
El primer subproceso para bloquear el mutex tendrá acceso al código interno. Si un segundo hilo intenta llamar a la función lock (), se bloqueará hasta que el primer hilo lo desbloquee. Entonces, un mutex es una función de bloqueo, a diferencia de un ciclo while. Las funciones de bloqueo no ejercerán presión sobre la CPU.
fuente
std::lock_guard
o similar, no.lock()
/.unlock()
. ¡RAII no es solo para la administración de memoria!