Emulando una barrera de memoria en Java para deshacerse de las lecturas volátiles

8

Supongamos que tengo un campo al que se accede simultáneamente y se lee muchas veces y rara vez se escribe.

public Object myRef = new Object();

Digamos que un subproceso T1 establecerá myRef en otro valor, una vez por minuto, mientras que otros subprocesos leerán myRef miles de millones de veces de forma continua y simultánea. Solo necesito que myRef sea finalmente visible para todos los hilos.

Una solución simple sería usar una referencia atómica o simplemente volátil como este:

public volatile Object myRef = new Object();

Sin embargo, las lecturas volátiles afaik incurren en un costo de rendimiento. Sé que es minúsculo, esto se parece más a algo que me pregunto que a algo que realmente necesito. Así que no nos preocupemos por el rendimiento y asumamos que esta es una pregunta puramente teórica.

Entonces, la pregunta se reduce a: ¿Hay alguna forma de evitar con seguridad las lecturas volátiles para referencias que rara vez se escriben, haciendo algo en el sitio de escritura?

Después de leer un poco, parece que las barreras de memoria podrían ser lo que necesito. Entonces, si existiera una construcción como esta, mi problema se resolvería:

  • Escribir
  • Invocar barrera (sincronización)
  • Todo está sincronizado y todos los hilos verán el nuevo valor. (sin un costo permanente en los sitios de lectura, puede ser obsoleto o incurrir en un costo único a medida que se sincronizan los cachés, pero después de eso todo vuelve al campo normal hasta la próxima escritura).

¿Existe tal construcción en Java, o en general? En este punto no puedo evitar pensar que si existiera algo así, ya habría sido incorporado a los paquetes atómicos por las personas mucho más inteligentes que los mantienen. (¿La lectura vs escritura desproporcionadamente frecuente podría no haber sido un caso para cuidar?) Entonces, ¿tal vez hay algo mal en mi pensamiento y tal construcción no es posible en absoluto?

He visto que algunos ejemplos de código usan 'volátil' para un propósito similar, explotando su contrato anterior. Hay un campo de sincronización separado, por ejemplo:

public Object myRef = new Object();
public volatile int sync = 0;

y al escribir hilo / sitio:

myRef = new Object();
sync += 1 //volatile write to emulate barrier

No estoy seguro de que esto funcione, y algunos argumentan que esto funciona solo en la arquitectura x86. Después de leer secciones relacionadas en JMS, creo que solo está garantizado que funcione si esa escritura volátil se combina con una lectura volátil de los hilos que necesitan ver el nuevo valor de myRef. (Entonces no se deshace de la lectura volátil).

Volviendo a mi pregunta original; ¿Es esto posible? ¿Es posible en Java? ¿Es posible en una de las nuevas API en Java 9 VarHandles?

sydnal
fuente
1
Para mí, parece que estás bien en el territorio donde necesitas escribir y ejecutar algunos puntos de referencia reales que simulen tus cargas de trabajo.
NPE
1
El JMM afirma que si su hilo de escritor lo hace sync += 1;y los hilos de su lector leen el syncvalor, también verán la myRefactualización. Debido a que solo necesita que los lectores vean la actualización eventualmente , podría usar esto para su ventaja para leer solo la sincronización en cada 1000 iteración del hilo del lector, o algo similar. Pero también puede hacer un truco similar volatile: simplemente guarde myRefen caché el campo en los lectores durante 1000 iteraciones, luego
léalo
@ PetrJaneček ¿Pero no tiene que sincronizar el acceso a la variable de contador que se comparte entre los hilos? ¿No será un cuello de botella? En mi opinión, eso sería aún más costoso.
Ravindra Ranwala
@RavindraRanwala Cada lector tendrá su propio contador, si te refieres a contar hasta las 1000 iteraciones más o menos. Si se refería al synccampo, no, los lectores no tocarían el synccampo en cada iteración, lo harían de manera oportunista, cuando quieran verificar si ha habido una actualización. Dicho esto, una solución más simple sería almacenar en caché myRefdurante 1000 rondas, luego volver a leerlo ...
Petr Janeček
@ PetrJaneček gracias, lo he pensado como una posible solución. Pero me pregunto si esto es posible utilizando una implementación sólida y genérica.
sydnal

Respuestas:

2

Entonces, básicamente, desea la semántica de un volatilesin el costo de tiempo de ejecución.

No creo que sea posible.

El problema es que el costo de tiempo de ejecución de volatile se debe a las instrucciones que implementan las barreras de memoria en el escritor y el código del lector. Si "optimiza" al lector al deshacerse de su barrera de memoria, ya no tiene la garantía de que el lector verá el nuevo valor "rara vez escrito" cuando realmente se escriba.

FWIW, algunas versiones de la sun.misc.Unsafeclase proporcionan métodos y explícitos loadFence, pero no creo que su uso proporcione ningún beneficio de rendimiento sobre el uso de a .storeFencefullFencevolatile


Hipotéticamente ...

lo que quiere es que un procesador en un sistema multiprocesador pueda decirle a todos los otros procesadores:

"¡Oye! Hagas lo que hagas, invalida tu caché de memoria para la dirección XYZ, y hazlo ahora".

Desafortunadamente, los ISA modernos no son compatibles con esto.

En la práctica, cada procesador controla su propio caché.

Stephen C
fuente
Ya veo, esa parte hipotéticamente en tu respuesta es lo que estaba buscando. Gracias.
sydnal
0

No estoy muy seguro de si esto es correcto, pero podría resolverlo usando una cola.

Cree una clase que envuelva un atributo ArrayBlockingQueue. La clase tiene un método de actualización y un método de lectura. El método de actualización publica el nuevo valor en la cola y elimina todos los valores, excepto el último valor. El método de lectura devuelve el resultado de una operación de espiar en la cola, es decir, leer pero no eliminar. Los hilos que miran el elemento en la parte delantera de la cola lo hacen sin obstáculos. Los hilos que actualizan la cola lo hacen limpiamente.

djhallx
fuente
0
  • Puede usar el ReentrantReadWriteLockque está diseñado para pocas escrituras, escenario de muchas lecturas.
  • Puede usar el StampedLockque está diseñado para el mismo caso de pocas escrituras y muchas lecturas, pero también las lecturas se pueden intentar de manera optimista. Ejemplo:

    private StampedLock lock = new StampedLock();
    
    public void modify() {            // write method
        long stamp = lock.writeLock();
        try {
          modifyStateHere();
        } finally {
          lock.unlockWrite(stamp);
        }
    } 
    
    public Object read() {            // read method
      long stamp = lock.tryOptimisticRead();
      Object result = doRead();       //try without lock, method should be fast
      if (!lock.validate(stamp)) {    //optimistic read failed
        stamp = lock.readLock();      //acquire read lock and repeat read
        try {
          result = doRead();
        } finally {
          lock.unlockRead(stamp);
        }
      }
      return result;
    }
  • Haga que su estado sea inmutable y permita modificaciones controladas solo clonando el objeto existente y alterando solo las propiedades necesarias a través del constructor. Una vez que se construye el nuevo estado, lo asigna a la referencia que está siendo leída por los muchos hilos de lectura. De esta manera, la lectura de hilos tiene un costo cero .

diginoise
fuente
si tiene ganas de votar, explique por qué, para que el autor y la comunidad puedan aprender
diginoise
Hacerlo inmutable no es posible en mi escenario. Y me sorprendería bastante si la caja de la cerradura estampada incurre en menos costo que una simple lectura volátil. Sin embargo lo intentaré, gracias.
sydnal
0

X86 proporciona TSO; obtienes vallas [LoadLoad] [LoadStore] [StoreStore] gratis.

Una lectura volátil requiere semántica de lanzamiento.

r1=Y
[LoadLoad]
[LoadStore]
...

Y como puede ver, el X86 ya lo proporciona de forma gratuita.

En su caso, la mayoría de las llamadas son de lectura y la línea de caché ya estará en el caché local.

Hay un precio que pagar por las optimizaciones a nivel de compilador, pero a nivel de hardware, una lectura volátil es tan costosa como una lectura normal.

Por otro lado, la escritura volátil es más costosa porque requiere un [StoreLoad] para garantizar la coherencia secuencial (en la JVM esto se hace usando lock addl %(rsp),0un MFENCE). Dado que las escrituras rara vez se encuentran en su situación, este no es un problema.

Tendría cuidado con las optimizaciones en este nivel porque es muy fácil hacer que el código sea más complejo de lo que realmente se necesita. Lo mejor es guiar sus esfuerzos de desarrollo por algunos puntos de referencia, por ejemplo, utilizando JMH y preferiblemente probarlo en hardware real. También podría haber otras criaturas desagradables ocultas como el intercambio falso.

pveentjer
fuente