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:
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.
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.
fuente
Respuestas:
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.
fuente
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.
fuente
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 un
ReaderWriterLockSlim
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
delegate
objeto 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.fuente
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:
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:
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.fuente