¿Cómo debo estructurar mis clases para permitir la simulación multiproceso?

8

En mi juego, hay terrenos con edificios (casas, centros de recursos). Los edificios como las casas tienen inquilinos, habitaciones, complementos, etc., y hay varios valores que deben simularse en función de todas estas variables.

Ahora, me gustaría usar AndEngine para las cosas de front-end, y crear otro hilo para hacer los cálculos de simulación (quizás también más tarde incluya AI en este hilo). Esto es para que un hilo completo no haga todo el trabajo y cause problemas como el bloqueo. Esto introduce el problema de concurrencia y dependencia .

El problema de la moneda es mi hilo de interfaz de usuario principal y el hilo de cálculo necesitaría acceder a todos los objetos de simulación. Así que tengo que hacerlos seguros para subprocesos, pero no sé cómo almacenar y estructurar los objetos de simulación para permitir eso.

El problema de dependencia es que para calcular valores, mis cálculos dependen de los valores de otros objetos.

¿Cuál sería la mejor manera de vincular mi objeto de inquilino en el edificio con mis cálculos? ¿Codificarlo en la clase de inquilino? ¿Cuál es una buena manera de hacer algoritmos de "almacenamiento" para que se puedan modificar fácilmente?

Una manera simple y perezosa sería juntar todo en una clase que contenga todo el objeto, como parcelas de tierra (que a su vez sostienen los edificios, etc.). Esta clase también mantendría el estado del juego, como la tecnología disponible para el usuario, grupos de objetos para cosas como sprites. Pero esta es una manera perezosa y peligrosa, ¿correcto?

Editar: Estaba mirando la inyección de dependencia, pero ¿qué tan bien se las arregla como una clase que contiene otros objetos? es decir, mi parcela de tierra, con un edificio, que tiene un inquilino y una gran cantidad de otros valores. DI también parece un dolor en el trasero con AndEngine.

NiffyShibby
fuente
Solo una nota rápida, no hay preocupación por el acceso concurrente a los datos si uno de los accesos solo es de solo lectura. Mientras mantenga su representación solo leyendo los datos sin procesar para usarlos para la representación y sin actualizar los datos durante su procesamiento, no hay problema. Un hilo actualiza los datos, el otro hilo simplemente lo lee y lo procesa.
James
Bueno, el acceso de concurrencia sigue siendo un problema, ya que el usuario puede comprar un terreno, construir un edificio en ese terreno y poner un inquilino en una casa, por lo que el hilo principal está creando datos y puede modificarlos. El acceso concurrente no es tanto un problema, sino más bien su instancia de compartir entre el hilo principal y el hilo secundario.
NiffyShibby
Hablo de la dependencia como un problema, parece que personas como Google Guice piensan que ocultar la dependencia no es una cosa sabia. Mis cálculos de inquilinos dependen de la parcela del edificio, la construcción, la creación de un sprite en la pantalla (podría tener una relación relacional entre un inquilino del edificio y la creación de un sprite de inquilino en otro lugar)
NiffyShibby
Supongo que mi sugerencia debe interpretarse como hacer que las cosas que se enhebran sean cosas que son independientes o que requieren acceso de solo lectura a los datos administrados por otro hilo. La representación sería un ejemplo de algo que podría desencadenar como lo haría solo necesita acceso de lectura a los datos para que pueda mostrarlos.
James
1
James, incluso un acceso de solo lectura puede ser una mala idea si otro hilo está en medio de hacer cambios en ese objeto. Con una estructura de datos compleja, podría provocar un bloqueo, y con tipos de datos simples podría provocar una lectura inconsistente.
Kylotan

Respuestas:

4

Su problema es inherentemente serial: debe completar una actualización de la simulación antes de poder procesarla. Descargar la simulación a un subproceso diferente simplemente significa que el subproceso de la interfaz de usuario principal no hace nada mientras el subproceso de simulación funciona (lo que significa que está bloqueado).

La "práctica recomendada" más común para la concurrencia es no poner su representación en un hilo y su simulación en otro, como está proponiendo. Realmente recomiendo contra ese enfoque, de hecho. Las dos operaciones están naturalmente relacionadas en serie, y aunque pueden ser forzadas, no es óptimo y no escala .

Un mejor enfoque es hacer que partes de la actualización o representación sean simultáneas, pero dejar que la actualización y la representación sean siempre en serie. Entonces, por ejemplo, si tiene un límite natural en su simulación (por ejemplo, si las casas nunca se afectan entre sí en su simulación), puede empujar todas las casas en cubos de N casas, y girar un montón de hilos que cada uno procesa y deje que esos hilos se unan antes de que se complete el paso de actualización. Esto escala mucho mejor y se adapta mucho mejor al diseño concurrente.

Estás pensando demasiado el resto del problema:

La inyección de dependencia es una pista falsa aquí: toda inyección de dependencia realmente significa que usted pasa ("inyecta") las dependencias de una interfaz a instancias de esa interfaz, generalmente durante la construcción.

Eso significa que si tiene una clase que modela a House, que necesita saber cosas sobre el estado en Cityque se encuentra, entonces el Houseconstructor podría verse así:

public House( City containingCity ) {
  m_city = containingCity; // Store in a member variable for later access
  ...
}

Nada especial.

El uso de un singleton es innecesario (a menudo lo ves hecho en algunos de los "marcos DI" increíblemente complejos y sobredimensionados como Caliburn que están diseñados para aplicaciones GUI "empresariales"; esto no lo convierte en una buena solución). De hecho, la introducción de singletons es a menudo la antítesis de una buena gestión de dependencias. También pueden causar serios problemas con el código multiproceso porque generalmente no se pueden hacer seguros para subprocesos sin bloqueos: cuantos más bloqueos deba adquirir, peor será su problema para manejarlo de forma paralela.


fuente
Recuerdo haber dicho que los singletons eran malos en mi publicación original ...
NiffyShibby
Recuerdo haber dicho que los singletons eran malos en mi publicación original, pero eso fue eliminado. Creo que entiendo lo que dices. Por ejemplo, mi pequeña persona está caminando por una pantalla, mientras hace que se llame al hilo de actualización, necesita actualizarlo, pero no puede porque el hilo principal está usando el objeto, por lo tanto, mi otro hilo está bloqueado. Donde como debería estar actualizando entre renderizado.
NiffyShibby
Alguien me ha enviado un enlace útil. gamedev.stackexchange.com/questions/95/…
NiffyShibby
5

La solución habitual para problemas de concurrencia es el aislamiento de datos .

El aislamiento significa que cada subproceso tiene sus propios datos y no toca los datos de otros subprocesos. De esta manera no hay problemas con la concurrencia ... pero luego tenemos problemas de comunicación. ¿Cómo pueden estos hilos trabajar juntos si no comparten ningún dato?

Hay dos enfoques aquí.

El primero es la inmutabilidad . Las estructuras / variables inmutables son las que nunca cambian su estado. Al principio, esto puede sonar inútil: ¿cómo se puede usar una "variable" que nunca cambia? Sin embargo, ¡podemos intercambiar estas variables! Considere este ejemplo: suponga que tiene una Tenantclase con un montón de campos, que debe estar en algún estado consistente. Si cambia un Tenantobjeto en el hilo A, y al mismo tiempo lo observa desde el hilo B, el hilo B puede ver el objeto en estado inconsistente. Sin embargo, si Tenantes inmutable, el hilo A no puede cambiarlo. En cambio, crea nuevos Tenantobjeto con campos configurados según sea necesario, y lo intercambia con el anterior. El intercambio es solo un cambio a una referencia, que probablemente sea atómica, y por lo tanto no hay forma de observar el objeto en estado inconsistente.

El segundo enfoque es la mensajería . La idea detrás de esto es que cuando todos los datos son "propiedad" de algún hilo, podemos decirle a este hilo qué hacer con los datos. Cada subproceso en esta arquitectura tiene una cola de mensajes (una lista de Messageobjetos y una bomba de mensajería) que ejecuta constantemente un método que elimina un mensaje de la cola, lo interpreta y llama a algún método de controlador. Por ejemplo, suponga que hizo tapping en una parcela de tierra, lo que indica que debe comprarse. El subproceso de la interfaz de usuario no puede cambiar el Plotobjeto directamente, porque pertenece al subproceso lógico (y probablemente sea inmutable). Por lo tanto, el subproceso de interfaz de usuario construye un BuyMessageobjeto y lo agrega a la cola del subproceso lógico. El hilo lógico, cuando se ejecuta, toma el mensaje de la cola y llamaBuyPlot(), extrayendo los parámetros del objeto de mensaje. Es posible que envíe un mensaje de vuelta, por ejemplo BuySuccessfulMessage, indicando al subproceso de la interfaz de usuario que coloque un mensaje "¡Ahora tienes más tierra!" ventana en pantalla. Por supuesto, el acceso a la cola de mensajes debe estar sincronizado con el bloqueo, la sección crítica o como se llame en AndEngine. Pero este es un punto único de sincronización entre subprocesos, y los subprocesos se suspenden por un tiempo muy corto, por lo que no es un problema.

Estos dos enfoques se utilizan mejor en combinación. Sus hilos deben comunicarse con mensajes y tener algunos datos inmutables "abiertos" para otros hilos, por ejemplo, una lista inmutable de diagramas para que la interfaz de usuario los dibuje.

¡Tenga en cuenta también que "solo lectura" no significa necesariamente inmutable ! Cualquier estructura de datos compleja como una tabla hash puede cambiar su estado interno en los accesos de lectura, por lo tanto, consulte primero la documentación.

No importa
fuente
Eso suena muy bien, tendré que hacer algunas pruebas con él, suena bastante costoso de esta manera, estaba pensando en la línea de DI con un alcance de singleton, luego uso bloqueos para el acceso concurrente. Pero nunca pensé en hacerlo de esta manera, podría funcionar: D
NiffyShibby
Bueno, así es como hacemos concurrencia en un servidor multiproceso altamente concurrente. Probablemente un poco exagerado para un juego simple, pero ese es el enfoque que usaría yo mismo.
importa
4

Probablemente el 99% de los programas de computadora escritos en la historia usaron solo 1 hilo y funcionaron bien. No tengo ninguna experiencia con AndEngine, pero es muy raro encontrar sistemas que requieran subprocesos, solo algunos que podrían haberse beneficiado con el hardware adecuado.

Tradicionalmente, para hacer simulación y GUI / renderizado en un hilo, simplemente hace un poco de la simulación, luego renderiza y repite, generalmente muchas veces por segundo.

Cuando alguien tiene poca experiencia en el uso de múltiples procesos, o no aprecia completamente lo que significa 'seguridad' de hilo (que es un término vago que puede significar muchas cosas diferentes), es demasiado fácil introducir muchos errores en un sistema. Por lo tanto, personalmente recomendaría adoptar el enfoque de un solo subproceso, intercalar la simulación y el renderizado, y guardar cualquier subproceso para operaciones que sabe con certeza que llevarán mucho tiempo y requerirán subprocesos y no un modelo basado en eventos.

Kylotan
fuente
Andengine hace el render por mí, pero sigo sintiendo que los cálculos tienen que ir en otro hilo, ya que el hilo principal de la interfaz de usuario terminaría desacelerándose si no se bloquea si todo se hace en un hilo.
NiffyShibby
¿Por qué sientes eso? ¿Tienes cálculos que son más caros que un juego 3D típico? ¿Y sabe que la mayoría de los dispositivos Android solo tienen 1 núcleo y, por lo tanto, no obtienen ningún beneficio intrínseco de rendimiento de los hilos adicionales?
Kylotan
No, pero es bueno separar la lógica y definir claramente lo que se está haciendo, si lo mantiene en el mismo hilo, tendría que hacer referencia a la clase principal donde se lleva a cabo o hacer algo de DI con alcance singleton. Lo cual no es tanto un problema. En cuanto al núcleo, estamos viendo que salen más dispositivos Android de doble núcleo, mi idea de juego podría no funcionar del todo bien en un dispositivo de un solo núcleo, mientras que en un doble núcleo podría funcionar bastante bien.
NiffyShibby
Así que diseñar todo en 1 hilo completo no me parece una gran idea, al menos con los hilos puedo separar la lógica y en el futuro no tener que preocuparme por tratar de mejorar el rendimiento como lo he diseñado desde el principio.
NiffyShibby
Pero su problema sigue siendo intrínsecamente serial, por lo que es probable que bloquee ambos subprocesos esperando que se unan de todos modos, a menos que haya aislado los datos (dando al subproceso algo que hacer mientras el subproceso lógico funciona) y renderizando un marco más o menos detrás de la simulación. El enfoque que está describiendo no es la mejor práctica comúnmente aceptada para el diseño de concurrencia.