Al desarrollar una pieza de software, ¿cuándo comienza a pensar / diseñar las secciones concurrentes?


8

Siguiendo el principio de no optimizar demasiado pronto, me pregunto en qué punto del diseño / desarrollo de un software comienza a pensar en las oportunidades de concurrencia.

Puedo imaginar que una estrategia sería escribir una sola aplicación de subprocesos y, a través de la creación de perfiles, identificar las secciones que son candidatas para ejecutarse en paralelo. Otra estrategia que he visto un poco es considerar el software por grupos de tareas y hacer que las tareas independientes sean paralelas.

Parte de la razón para preguntar es que, por supuesto, si espera hasta el final y solo refactoriza el software para que funcione simultáneamente, puede estructurar las cosas de la peor manera posible y tener una tarea importante en sus manos.

¿Qué experiencias han ayudado a determinar cuándo considera la paralelización en su diseño?

Respuestas:


7

Lo que pasa con las tareas de subproceso es que desea una alta cohesión, un bajo acoplamiento y una buena encapsulación. Curiosamente, también son objetivos de diseño dignos para aplicaciones de un solo subproceso. Tuve una tarea hoy que originalmente no tenía planeado paralelizar, pero cuando lo hice, implicó poco más que renombrar una función run()y cambiar cómo se llamaba.

No optimizar demasiado pronto significa no poner todo en un hilo "por si acaso", pero tampoco debe pintarse en un rincón arquitectónico, por lo que será demasiado difícil de optimizar si surge la necesidad.


Sí, las funciones reentrantes, las colas de trabajo y similares pueden ayudar en aplicaciones de subproceso único y en aplicaciones de subprocesos múltiples, pero también permiten una buena extensibilidad hacia el procesamiento paralelo más adelante. Lo que ahorra mucho dolor de cabeza con problemas de sincronización más tarde.
Codificador

2

Los programadores de Java deberían adoptar la Callableinterfaz para las unidades de trabajo. Si toda su aplicación consiste en un ciclo de creación de Callables, envío de todas las unidades a un Ejecutor, manejo de tareas posteriores a la generación, tiene algo que se puede configurar fácilmente en el procesamiento en serie, "tres colas de trabajo" y "hacer todo a la vez "simplemente eligiendo al albacea correcto.

Estamos adaptando lentamente este patrón, ya que es muy común que el enfoque en serie sea demasiado lento en un punto y luego debemos hacerlo de todos modos.


1

Varía según el proyecto. A veces es muy fácil ver qué se puede hacer en paralelo: quizás su programa procese lotes de archivos. Suponga que el procesamiento de cada archivo es completamente independiente de todos los demás archivos, por lo que puede ser bastante obvio que puede procesar 1 archivo a la vez, o 10, o 100, y ninguno de estos trabajos afectará al otro.

Se vuelve un poco más complicado cuando los posibles trabajos paralelos no son los mismos. Al procesar un archivo de imagen, podría tener un trabajo que crea un histograma, otro que produce una miniatura y tal vez otro que extrae metadatos EXIF ​​y luego un trabajo final que toma la salida de todos estos trabajos y los almacena en una base de datos. En este ejemplo, tal vez no esté claro si deben ejecutarse en paralelo o si deberían (el último trabajo tendrá que esperar a que los trabajos anteriores se completen con éxito).

En mi experiencia, la forma más fácil de paralelizar algo es buscar procesos que puedan ejecutarse de la manera más independiente posible (como en el primer ejemplo) y comenzar con ellos. Solo trataría de hacer que el segundo ejemplo se ejecute en paralelo si pensara que obtendría una ganancia significativa en el rendimiento con él.


1

Debe diseñar la concurrencia en su aplicación desde el principio. Normalmente, como optimización, estaría de acuerdo en que debería dejarse hasta más tarde si no es inherentemente obvio. El problema es que la simultaneidad puede requerir una nueva arquitectura de su aplicación desde cero en el peor de los casos: algunos sistemas son prácticamente imposibles de agregar. Un ejemplo sencillo de esto son los sistemas que comparten datos, por ejemplo, los aspectos de simulación y representación de un juego.


0

Diría que el enhebrado es parte de la arquitectura de la aplicación. Por lo tanto, es una de las primeras cosas en las que debo pensar.

Por ejemplo, cuando hago una aplicación GUI, el código GUI es de un solo subproceso, por lo que las tareas de ejecución prolongada (por ejemplo, el procesamiento XML) bloquearían la GUI y deberían ejecutarse en un subproceso en segundo plano.

Por ejemplo, un servidor estaría basado en subprocesos, donde cada solicitud es manejada por un nuevo subproceso, o el servidor podría ser controlado por eventos y usar solo un subproceso por cpu-core, pero nuevamente, las tareas de ejecución prolongada deberían ejecutarse en un hilo de fondo o se divide en tareas más pequeñas.


0

Con la forma en que abordo las cosas, el tipo de subprocesamiento múltiple es gratuito y relativamente sencillo de aplicar en retrospectiva. Pero primero estoy pensando en los datos. No sé si esto funciona para todos los dominios, pero intentaré cubrir cómo lo hago.

Entonces, primero se trata del tipo de datos más grueso requerido para el software que se procesará con frecuencia. Si es un juego que podría ser mallas, sonidos, movimiento, emisores de partículas, luces, texturas, cosas de este tipo. Y, por supuesto, hay mucho en qué pensar si profundiza solo en mallas y piensa en cómo deberían representarse, pero lo omitiremos por ahora. En este momento estamos pensando en el nivel arquitectónico más amplio.

Y mi primer pensamiento es: "¿Cómo unificamos la representación de todas estas cosas para que podamos lograr un patrón de acceso relativamente uniforme para todos estos tipos de cosas?" Y mi primer pensamiento podría ser almacenar cada tipo de cosas en su propia matriz contigua con una forma de lista libre para reclamar espacios vacíos. Y eso tiende a unificar la API para que podamos usar más fácilmente, por ejemplo, el mismo tipo de código para serializar mallas como hacemos luces y texturas, al menos en cuanto a dónde y cómo se accede a estos componentes. Cuanto más podamos unificar cómo se representa todo, más el código que accede a esas cosas tiende a tomar una forma uniforme.

Eso es genial. Ahora también podemos señalar estas cosas con índices de 32 bits y solo tomar la mitad de la memoria de un puntero de 64 bits. Y oye, ahora podemos establecer intersecciones en tiempo lineal si podemos asociar un conjunto de bits paralelo, por ejemplo, también podemos asociar datos a cualquiera de estas cosas de forma muy económica en paralelo ya que estamos indexando todo. Ah, y ese conjunto de bits puede devolvernos un conjunto de índices ordenados para recorrer en orden secuencial para mejorar los patrones de acceso a la memoria, sin tener que volver a cargar la misma línea de caché varias veces en un solo bucle. Podemos probar 64 bits a la vez. Si no se establecen todos los 64 bits, podemos omitir más de 64 elementos a la vez. Si todos están configurados, podemos procesarlos todos a la vez. Si algunos están configurados pero no todos, podemos usar las instrucciones de FFS para determinar rápidamente qué bits están configurados.

Pero, espera, eso es algo costoso si solo quisiéramos asociar datos a unos cientos de miles de cosas. Entonces usemos una matriz dispersa, así:

ingrese la descripción de la imagen aquí

Y oye, ahora que tenemos todo almacenado en matrices dispersas e indexándolas, sería bastante fácil hacer de esta una estructura de datos persistente.

ingrese la descripción de la imagen aquí

Ahora podemos escribir funciones más baratas sin efectos secundarios, ya que no necesitan copiar en profundidad lo que no ha cambiado.

Y aquí ya me dieron una hoja de trucos después de aprender sobre los motores ECS, pero ahora pensemos en qué tipo de funciones amplias deberían estar operando en cada tipo de componente. Podemos llamar a estos "sistemas". El "SoundSystem" puede procesar componentes "Sound". Cada sistema es una función amplia que opera en uno o más tipos de datos.

ingrese la descripción de la imagen aquí

Eso nos deja con muchos casos en los que, para cualquier tipo de componente dado, solo uno o dos sistemas generalmente accederán a ellos. Hmm, eso parece seguro que ayudaría con la seguridad del hilo y reduciría al mínimo la contención del hilo.

Además, trato de pensar en cómo hacer pases homogéneos sobre los datos. En lugar de como:

for each thing:
    play with it
    cuddle it
    kill it

Intento dividirlo en múltiples pases más simples:

for each thing:
    play with it
for each thing:
    cuddle it
for each thing:
    kill it

Eso a veces requiere almacenar algún estado intermedio para el siguiente paso diferido homogéneo para procesar, pero descubrí que realmente me ayuda a mantener y razonar sobre el código, sabiendo que cada ciclo tiene una lógica más simple y más uniforme. Y oye, eso parece que simplificaría la seguridad del hilo y reduciría la contención del hilo.

Y simplemente sigue así hasta que encuentra que tiene una arquitectura que es realmente fácil de paralelizar con confianza sobre la seguridad y la corrección de su hilo, pero todo inicialmente con el enfoque de unificar representaciones de datos, tener patrones de acceso a memoria más predecibles, reduciendo uso de memoria, simplificando los flujos de control a pasos más homogéneos, reduciendo la cantidad de funciones en su sistema que causan efectos secundarios sin incurrir en costos de copia profunda muy costosos, unificando su API, etc.

Cuando combina todas estas cosas, tiende a terminar con un sistema que minimiza la cantidad de estado compartido en el que tropezó con un diseño que es realmente amigable para la concurrencia. Y si es necesario compartir algún estado, a menudo encuentra que no tiene mucha contención donde es barato usar alguna sincronización sin causar un atasco de tráfico, y además, a menudo puede ser manejado por su estructura de datos central que unifica la representación de todas las cosas en el sistema para que no tenga que aplicar sincronizaciones de hilos a cientos de lugares diferentes, solo a un puñado.

Ahora, cuando profundizamos en uno de los componentes más complejos, como las mallas, repetimos el mismo proceso de diseño, comenzando por pensar primero en los datos. Y si lo hacemos bien, incluso podríamos ser capaces de paralelizar fácilmente el procesamiento de una sola malla, pero el diseño arquitectónico más amplio que establecimos ya nos permite paralelizar el procesamiento de múltiples mallas.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.