¿Cómo puedo hacer que el mensaje que pasa entre hilos en un motor multiproceso sea menos engorroso?

18

El motor C ++ en el que estoy trabajando actualmente se divide en varios subprocesos grandes: Generación (para crear mi contenido de procedimiento), Juego (para IA, scripts, simulación), Física y Renderizado.

Los hilos se comunican entre sí a través de pequeños objetos de mensaje, que pasan de hilo a hilo. Antes de dar un paso, un hilo procesa todos sus mensajes entrantes: actualizaciones para transformaciones, agregar y quitar objetos, etc.

Al principio del proceso y he notado un par de cosas:

  1. El sistema de mensajería es engorroso. Crear un nuevo tipo de mensaje significa subclasificar la clase de mensaje base, crear una nueva enumeración para su tipo y escribir lógica sobre cómo los hilos deberían interpretar el nuevo tipo de mensaje. Es un obstáculo para el desarrollo y es propenso a errores tipográficos. (Nota: ¡trabajar en esto me hace apreciar lo geniales que pueden ser los lenguajes dinámicos!)

    ¿Hay una mejor manera de hacer esto? ¿Debo usar algo como boost :: bind para hacer esto automático? Me preocupa que si hago eso pierdo la capacidad de decir, ordenar los mensajes según el tipo, o algo así. No estoy seguro si ese tipo de gestión será incluso necesaria.

  2. El primer punto es importante porque estos hilos comunican mucho. Crear y pasar mensajes es una gran parte de hacer que las cosas sucedan. Me gustaría simplificar ese sistema, pero también estar abierto a otros paradigmas que podrían ser tan útiles. ¿Hay diferentes diseños multiproceso en los que debería pensar para ayudar a que esto sea más fácil?

    Por ejemplo, hay algunos recursos que se escriben con poca frecuencia, pero que se leen con frecuencia desde varios subprocesos. ¿Debería estar abierto a la idea de tener datos compartidos, protegidos por mutexes, a los que pueden acceder todos los hilos?

Esta es mi primera vez diseñando algo con multihilo en mente desde cero. En esta etapa inicial, realmente creo que está yendo realmente bien (considerando), pero me preocupa la escala y mi propia eficiencia para implementar cosas nuevas.

Carne de rapaz
fuente
66
Realmente no hay una sola pregunta dirigida aquí, y como tal esta publicación no se ajusta bien al estilo de preguntas y respuestas de este sitio. Te recomendaría dividir tu publicación en publicaciones distintas, una por pregunta, y reenfocar las preguntas para que pregunten sobre un problema específico que realmente tienes en lugar de una vaga colección de consejos o sugerencias.
2
Si quieres entablar una conversación más general, te recomiendo que pruebes esta publicación en los foros de gamedev.net . Como dijo Josh, dado que su "pregunta" no es una sola pregunta específica, sería bastante difícil de acomodar en el formato StackExchange.
Cypher
Gracias por los comentarios chicos! Esperaba que alguien con más conocimiento pudiera tener un solo recurso / experiencia / paradigma que pudiera abordar varios de mis problemas a la vez. Tengo la sensación de que una gran idea podría sintetizar mis diversos problemas en una cosa que me falta, y estaba pensando que alguien con más experiencia de la que tengo podría reconocer eso ... Pero tal vez no, y de todos modos, puntos tomados !
Raptormeat
Cambié el nombre de su título para que sea más específico para la transmisión de mensajes, ya que las preguntas tipo "consejos" implican que no hay un problema específico que resolver (y, por lo tanto, en estos días cerraría como "no es una pregunta real").
Tetrad
¿Estás seguro de que necesitas hilos separados para la física y el juego? Esos dos parecen estar muy entrelazados. Además, es difícil saber cómo ofrecer asesoramiento sin saber cómo se comunican cada uno de ellos y con quién.
Nicol Bolas

Respuestas:

10

Para su problema más amplio, considere tratar de encontrar formas de reducir la comunicación entre subprocesos tanto como sea posible. Es mejor evitar por completo los problemas de sincronización, si puedes. Esto se puede lograr mediante el almacenamiento en búfer doble de sus datos, introduciendo una latencia de actualización única pero facilitando en gran medida la tarea de trabajar con datos compartidos.

Como comentario aparte, ¿ha considerado no enhebrar por subsistema, sino utilizar el desove de subprocesos o grupos de subprocesos para bifurcar por tarea? (consulte esto con respecto a su problema específico, para la agrupación de subprocesos). Este breve documento describe de forma concisa el propósito y el uso del patrón de agrupación. Ver estas respuestas informativasademás. Como se señaló allí, los grupos de subprocesos mejoran la escalabilidad como una ventaja. Y es "escribir una vez, usar en cualquier lugar", en lugar de tener que obtener hilos basados ​​en el subsistema para jugar bien cada vez que escribes un nuevo juego o motor. También hay muchas soluciones sólidas de agrupación de hilos de terceros. Sería más sencillo comenzar con el desove de subprocesos y luego avanzar hacia los grupos de subprocesos, si es necesario mitigar la sobrecarga del desove y la destrucción de subprocesos.

Ingeniero
fuente
1
¿Alguna recomendación para bibliotecas específicas de agrupación de subprocesos para verificar?
Imre
Nick: muchas gracias por la respuesta. En cuanto a su primer punto, creo que es una gran idea y probablemente la dirección en la que me moveré. Por el momento es lo suficientemente temprano como para que aún no sepa qué necesitaría tener doble amortiguación. Lo tendré en cuenta a medida que se solidifique con el tiempo. A su segundo punto, ¡gracias por la sugerencia! Sí, los beneficios de las tareas de subproceso son claros. Leeré tus enlaces y pensaré en ello. No estoy 100% seguro de si funcionará para mí / cómo hacer que funcione para mí, pero definitivamente lo pensaré seriamente. ¡Gracias!
Raptormeat
1
@imre echa un vistazo a la biblioteca Boost: tienen futuros, que son una forma agradable / fácil de abordar estas cosas.
Jonathan Dickinson el
5

Preguntaste sobre diferentes diseños de subprocesos múltiples. Un amigo mío me contó sobre este método que me pareció genial.

La idea es que habría 2 copias de cada entidad del juego (desperdicio, lo sé). Una copia sería la copia actual, y la otra sería la copia pasada. La copia actual es estrictamente de solo escritura , y la copia pasada es estrictamente de solo lectura . Cuando va a actualizar, asigna rangos de su lista de entidades a tantos hilos como mejor le parezca. Cada subproceso tiene acceso de escritura a las copias actuales en el rango asignado y cada subproceso tiene acceso de lectura a todas las copias anteriores de las entidades y, por lo tanto, puede actualizar las copias actuales asignadas utilizando datos de las copias anteriores sin bloqueo. Entre cada cuadro, la copia actual se convierte en la copia pasada, sin embargo, desea manejar el intercambio de roles.

John McDonald
fuente
4

Hemos tenido el mismo problema, solo con C #. Después de pensar mucho sobre la facilidad (o la falta de ella) de crear nuevos mensajes, lo mejor que pudimos hacer fue crear un generador de código para ellos. Es un poco feo, pero utilizable: dada solo una descripción del contenido del mensaje, genera clase de mensaje, enumeraciones, código de control de marcador de posición, etc. Todo este código es casi el mismo cada vez y es muy propenso a errores tipográficos.

No estoy del todo contento con esto, pero es mejor que escribir todo ese código a mano.

En cuanto a los datos compartidos, la mejor respuesta es, por supuesto, "depende". Pero, en general, si algunos datos se leen con frecuencia y muchos hilos los necesitan, compartirlos vale la pena. Para la seguridad del hilo, su mejor opción es hacerlo inmutable , pero si eso está fuera de discusión, mutex podría hacerlo. En C # hay unReaderWriterLockSlim clase, diseñada específicamente para tales casos; Estoy seguro de que hay un equivalente de C ++.

Otra idea para la comunicación de hilos, que probablemente resuelva su primer problema, es pasar controladores en lugar de mensajes. No estoy seguro de cómo resolver esto en C ++, pero en C # puede enviar un delegateobjeto a otro hilo (como en, agregarlo a algún tipo de cola de mensajes), y llamar a este delegado desde el hilo receptor. Esto hace posible crear mensajes "ad hoc" en el acto. Solo jugué con esta idea, en realidad nunca la probé en producción, por lo que de hecho podría resultar mal.

No importa
fuente
¡Gracias por toda la estupenda información! La última parte sobre los controladores es similar a lo que mencioné sobre el uso de enlaces o functores para pasar funciones. Me gusta un poco la idea: podría probarlo y ver si apesta o es increíble: D Podría comenzar simplemente creando una clase CallDelegateMessage y sumergiendo mi dedo del pie en el agua.
Raptormeat
1

Solo estoy en la fase de diseño de algún código de juego enhebrado, por lo que solo puedo compartir mis pensamientos, no ninguna experiencia real. Dicho esto, estoy pensando en las siguientes líneas:

  • La mayoría de los datos del juego deben compartirse para acceso de solo lectura .
  • Escribir datos es posible utilizando un tipo de mensaje.
  • Para evitar actualizar datos mientras otro hilo los lee, el ciclo del juego tiene dos fases distintas: leer y actualizar.
  • En la fase de lectura:
  • Todos los datos compartidos son de solo lectura para todos los hilos.
  • Los subprocesos pueden calcular cosas (utilizando el almacenamiento local de subprocesos) y generar solicitudes de actualización , que son básicamente objetos de comando / mensaje, colocados en una cola, para ser aplicados más tarde.
  • En la fase de actualización:
  • Todos los datos compartidos son de solo escritura. Los datos deben asumirse en un estado desconocido / inestable.
  • Aquí es donde se procesan los objetos de solicitud de actualización.

Creo que (aunque no estoy seguro) en teoría esto debería significar que durante las fases de lectura y actualización, cualquier número de subprocesos puede ejecutarse simultáneamente con una sincronización mínima. En la fase de lectura, nadie está escribiendo los datos compartidos, por lo que no deben surgir problemas de concurrencia. La fase de actualización, bueno, eso es más complicado. Actualizaciones paralelas sobre el mismo dato serían un problema, por lo que es necesario realizar alguna sincronización aquí. Sin embargo, aún podría ejecutar un número arbitrario de subprocesos de actualización, siempre que estén operando en diferentes conjuntos de datos.

En general, creo que este enfoque se prestaría bien a un sistema de agrupación de subprocesos. Las partes problemáticas son:

  • Sincronización de los hilos de actualización (asegúrese de que no haya varios hilos que intenten actualizar el mismo conjunto de datos).
  • Asegurándose de que en la fase de lectura ningún hilo pueda escribir accidentalmente datos compartidos. Me temo que habría demasiado espacio para errores de programación, y no estoy seguro de cuánto de ellos podrían ser fácilmente atrapados por las herramientas de depuración.
  • Escribir código de manera tal que no pueda confiar en que sus resultados intermedios estén disponibles para leer de inmediato. Es decir, no puedes escribir x += 2; if (x > 5) ...si x se comparte. Necesita hacer una copia local de x, o producir una solicitud de actualización, y solo hacer el condicional en la próxima ejecución. Esto último significaría una gran cantidad de código adicional para preservar el estado local de subprocesos.
imre
fuente