Hablar en el contexto de un juego basado en el renderizador de OpenGL:
Supongamos que hay dos hilos:
Actualiza la lógica y física del juego, etc. para los objetos del juego.
Realiza llamadas de sorteo de OpenGL para cada objeto del juego según los datos de los objetos del juego (ese hilo 1 se actualiza constantemente)
A menos que tenga dos copias de cada objeto del juego en el estado actual del juego, tendrá que pausar el Hilo 1 mientras que el Hilo 2 realiza las llamadas de sorteo; de lo contrario, los objetos del juego se actualizarán en medio de una llamada de sorteo para ese objeto, que es indeseable!
Pero detener el subproceso 1 para realizar llamadas de extracción con seguridad desde el subproceso 2 mata todo el propósito de subprocesamiento múltiple / concurrencia
¿Existe un mejor enfoque para esto que no sea usar cientos o miles o sincronizar objetos / cercas para que la arquitectura multinúcleo pueda ser explotada para el rendimiento?
Sé que todavía puedo usar subprocesos múltiples para cargar texturas y compilar sombreadores para los objetos que aún no forman parte del estado actual del juego, pero ¿cómo lo hago para los objetos activos / visibles sin causar conflictos con el dibujo y la actualización?
¿Qué sucede si uso un bloqueo de sincronización separado en cada objeto del juego? ¡De esta forma, cualquier subproceso solo se bloquearía en un objeto (caso ideal) en lugar de en todo el ciclo de actualización / dibujo! Pero, ¿qué tan costoso es bloquear las cerraduras de cada objeto (el juego puede tener mil objetos)?
fuente
Respuestas:
El enfoque que ha descrito, usando bloqueos, sería muy ineficiente y probablemente más lento que usar un solo hilo. El otro enfoque de mantener copias de datos en cada hilo probablemente funcionaría bien "en cuanto a velocidad", pero con un costo de memoria prohibitivo y complejidad de código para mantener las copias sincronizadas.
Hay varios enfoques alternativos para esto, una solución popular para el procesamiento de subprocesos múltiples es mediante el uso de un doble búfer de comandos. Esto consiste en ejecutar el back-end del renderizador en un hilo separado, donde se realizan todas las llamadas de dibujo y la comunicación con la API de representación. El subproceso de front-end que ejecuta la lógica del juego se comunica con el procesador de back-end a través de un búfer de comandos (doble búfer). Con esta configuración, solo tiene un punto de sincronización al finalizar un fotograma. Mientras que el front-end está llenando un búfer con comandos de renderizado, el back-end está consumiendo el otro. Si ambos hilos están bien equilibrados, ninguno debería morir de hambre. Sin embargo, este enfoque es subóptimo, ya que introduce latencia en los cuadros renderizados, además, es probable que el controlador OpenGL ya lo esté haciendo en su propio proceso, por lo que las ganancias de rendimiento tendrían que medirse cuidadosamente. También solo usa dos núcleos, en el mejor de los casos. Sin embargo, este enfoque se ha utilizado en varios juegos exitosos, como Doom 3 y Quake 3
Los enfoques más escalables que hacen un mejor uso de las CPU de varios núcleos son los que se basan en tareas independientes , donde se activa una solicitud asincrónica que se atiende en un subproceso secundario, mientras que el subproceso que activó la solicitud continúa con algún otro trabajo. La tarea idealmente no debería tener dependencias con los otros subprocesos, para evitar bloqueos (¡también evitar datos compartidos / globales como la peste!). Las arquitecturas basadas en tareas son más utilizables en partes localizadas de un juego, como animaciones informáticas, búsqueda de IA, generación de procedimientos, carga dinámica de accesorios de escena, etc. Los juegos están naturalmente llenos de eventos, la mayoría de los eventos son asíncronos, por lo que Es fácil hacer que se ejecuten en hilos separados.
Finalmente, recomiendo leer:
Conceptos básicos de subprocesos para juegos de Intel.
Concurrencia efectiva por Herb Sutter (varios enlaces a otros buenos recursos en esta página).
fuente