2012-06-26 24 views
6

decir que tengo un objeto de datos:grupo de subprocesos, datos compartidos, Sincronización Java

class ValueRef { double value; }

donde cada objeto de datos se almacena en una colección maestra:

Collection<ValueRef> masterList = ...;

también tener una colección de trabajos, donde cada trabajo tiene una colección local de objetos de datos (donde cada objeto de datos también aparece en el masterList):

class Job implements Runnable { 
    Collection<ValueRef> neededValues = ...; 
    void run() { 
     double sum = 0; 
     for (ValueRef x: neededValues) sum += x; 
     System.out.println(sum); 
    } 
} 

de casos de uso:

  1. for (ValueRef x: masterList) { x.value = Math.random(); }

  2. rellenar una cola de trabajos con algunos puestos de trabajo.

  3. despertar a un grupo de subprocesos

  4. Esperar a que cada puesto de trabajo se ha evaluado

Nota: Durante la evaluación del trabajo, todos los valores son todos constantes. Sin embargo, los subprocesos posiblemente hayan evaluado trabajos en el pasado y conserven valores en caché.

Pregunta: ¿Cuál es la cantidad mínima de sincronización necesaria para garantizar que cada thread vea los últimos valores?

Entiendo la sincronización desde el monitor/lock-perspective, no entiendo sincronizar desde el cache/flush-perspective (es decir, qué está garantizado por el modelo de memoria al entrar/salir del bloque sincronizado).

Para mí, parece que debería sincronizar una vez en el hilo que actualiza los valores para asignar los nuevos valores a la memoria principal y una vez por hilo de trabajo para vaciar la caché para que se lean los nuevos valores. Pero no estoy seguro de cómo hacer esto.

Mi enfoque: crear un monitor mundial: static Object guard = new Object(); A continuación, sincronizar el guard, mientras que la actualización de la lista maestra. Finalmente, antes de iniciar el grupo de subprocesos, una vez para cada subproceso del grupo, sincronice en guard en un bloque vacío.

¿Eso realmente causa una descarga total de cualquier valor leído por ese hilo? ¿O solo los valores tocados dentro del bloque de sincronización? En cuyo caso, en lugar de un bloque vacío, ¿debería leer cada valor una vez en un ciclo?

Gracias por su tiempo.


Editar: Creo que mi cuestión se reduce a, una vez que sale de un bloque sincronizado, no todos los primeros de lectura (después de ese punto) ir a la memoria principal? Independientemente de en qué sincronicé?

+0

Parece casi un lugar perfecto para tomar ventaja de la palabra clave volátil – ControlAltDel

+0

Escribo una vez (efectivamente constante), pero puedo leer millones de veces. Volátil nunca se almacena en la memoria caché localmente. Si crease el grupo de subprocesos cada vez, el código funcionaría bien sin sincronización/volátil (ya que no existiría el caché anterior). –

+0

No veo la necesidad de volátiles aquí. Si ValueRef es efectivamente inmutable, solo hazlo realmente inmutable. Use doble. Cree una nueva colección para cada trabajo antes de programarla y envuélvala en Colección no modificable (solo como recordatorio). ¿Qué problema tienes? –

Respuesta

3

No importa que los subprocesos de un grupo de subprocesos hayan evaluado algunos trabajos en el pasado.

Javadoc de Executor dice: efectos de consistencia

Memoria: Acciones en un hilo antes de presentar un objeto Ejecutable a un ejecutor suceder antes de que comience su ejecución, tal vez en otro hilo.

Por lo tanto, siempre que utilice la implementación estándar del grupo de subprocesos y cambie los datos antes de enviar los trabajos, no debe preocuparse por los efectos de visibilidad de la memoria.

+0

Esto se debe a ... en los hilos de trabajo, ¿hay un bloque de sincronización esperando nuevos trabajos? ¿Y cuando ese bloque sale, se borra todo el caché? ¿Podría sincronizar algo aleatorio y obtener el mismo efecto? –

+0

@AndrewRaffensperger: no importa cómo se implemente; hay una garantía y debe proporcionarse. En cuanto a la última pregunta, básicamente, pero no tiene sentido: sin medios de sincronización adicionales, no se puede decir que los bloques sincrónicos en los hilos de trabajo ejecutados después del bloque sincronizado en el hilo principal; con medios adicionales de sincronización es redundante. – axtavt

2

Lo que estás planeando suena suficiente. Depende de cómo planee "activar el grupo de subprocesos".

El modelo de memoria de Java proporciona que todas las escrituras realizadas por un subproceso antes de entrar en un bloque synchronized son visibles para los subprocesos que posteriormente se sincronizan en ese bloqueo.

Por lo tanto, si está seguro de los subprocesos de trabajo se bloquean en un wait() llamada (que debe estar dentro de un bloque synchronized) durante el tiempo de actualizar la lista maestra, cuando se despiertan y se convierte en ejecutable, las modificaciones hechas por el el hilo maestro será visible para estos hilos.

Sin embargo, le recomiendo aplicar las utilidades de concurrencia de mayor nivel en el paquete java.util.concurrent. Estos serán más sólidos que su propia solución, y son un buen lugar para aprender la concurrencia antes de profundizar.


Solo para aclarar: Es casi imposible controlar subprocesos de trabajo sin utilizar un bloque sincronizado, donde se realiza una comprobación para ver si el trabajador tiene una tarea de implementar. Por lo tanto, cualquier cambio realizado por el hilo del controlador en el trabajo ocurre, antes de que el hilo de trabajo se despierte. Necesita un bloque synchronized, o al menos una variable volatile para actuar como barrera de memoria; sin embargo, no puedo pensar cómo crearías un grupo de subprocesos con uno de estos.

Como ejemplo de las ventajas de utilizar el paquete java.util.concurrency, considere esto: se puede utilizar un bloque de synchronized con una llamada wait() en ella, o un bucle ocupado: espere con una variable volatile. Debido a la sobrecarga de la conmutación de contexto entre subprocesos, una espera ocupada puede funcionar mejor bajo ciertas condiciones. — no es necesaria la horrible idea que uno podría asumir a primera vista.

Si utiliza las utilidades de Concurrencia (en este caso, probablemente un ExecutorService), la mejor selección para su caso particular se puede hacer para usted, teniendo en cuenta el entorno, la naturaleza de la tarea y las necesidades de otros hilos en un momento dado. Lograr ese nivel de optimización es mucho trabajo innecesario.

+0

No puedo pagar la sobrecarga de java.util.concurrent. Los datos en mi ejemplo se actualizan una vez, luego se vuelven "constantes" durante la evaluación de subprocesos múltiples. Me interesa cómo esos datos se vuelven visibles para los otros hilos preexistentes. Parece que cualquier bloque de sincronización, incluso sin ninguna relación sincronizada de pasar antes que, causa esta visibilidad. O tal vez suceda: antes no requiere ninguna sincronización explícita y "no se ejecutan trabajos hasta que se realicen todos los cambios de valores" se ajusta a la ley. –

+1

@AndrewRaffensperger Derecha. Si eso es todo lo que necesita, existe una utilidad 'java.util.concurrent' con la sobrecarga mínima requerida para la corrección. Es un error suponer que las utilidades de Concurrencia tienen una sobrecarga mayor; de hecho, proporcionan acceso a herramientas de concurrencia de alto rendimiento como compare-and-swap. Implementar esto usted mismo en Java va a ser más lento que el código nativo optimizado detrás de las clases 'AtomicXXX'. Hay ventajas de rendimiento similares en la mayoría de las otras utilidades. – erickson

1

¿Por qué no haces que Collection<ValueRef> y ValueRef sean inmutables o al menos no modifiquen los valores en la colección después de haber publicado la referencia a la colección. Entonces no tendrás ninguna preocupación sobre la sincronización.

Ahí es cuando desea cambiar los valores de la colección, crear una nueva colección y poner nuevos valores en ella. Una vez que se han establecido los valores, pase la referencia de colección a los nuevos objetos de trabajo.

La única razón para no hacer esto sería si el tamaño de la colección es tan grande que apenas cabe en la memoria y no puede permitirse tener dos copias, o el intercambio de las colecciones causaría demasiado trabajo para el recolector de basura (compruebe que uno de estos es un problema antes de utilizar una estructura de datos mutable para el código de subprocesamiento).

+0

Correcto, siempre pude reconstruir ValueRef o reconstruir el grupo de subprocesos, y mi problema desaparece. Pero en mi implementación real, la estructura de datos es muy compleja, y se llama al código con la frecuencia suficiente para reconstruir el grupo de subprocesos. Cada evaluación sería demasiado sobrecargada. –

Cuestiones relacionadas