Emulando uma barreira de memória em Java para se livrar de leituras voláteis

8

Suponha que eu tenha um campo acessado simultaneamente e que seja lido muitas vezes e raramente gravado.

public Object myRef = new Object();

Digamos que um Thread T1 esteja configurando myRef para outro valor, uma vez por minuto, enquanto N outros Threads lerão myRef bilhões de vezes contínua e simultaneamente. Eu só preciso que myRef seja eventualmente visível para todos os threads.

Uma solução simples seria usar um AtomicReference ou simplesmente volátil como este:

public volatile Object myRef = new Object();

No entanto, leituras voláteis após um incorrer em um custo de desempenho. Eu sei que é minúsculo, é mais algo que eu imagino do que algo que eu realmente preciso. Portanto, não vamos nos preocupar com desempenho e suponha que isso seja uma questão puramente teórica.

Portanto, a pergunta se resume a: Existe uma maneira de ignorar com segurança leituras voláteis de referências que raramente são gravadas, fazendo algo no site de gravação?

Após algumas leituras, parece que as barreiras de memória podem ser o que eu preciso. Portanto, se um construto como esse existisse, meu problema seria resolvido:

  • Escreva
  • Invocar barreira (sincronização)
  • Tudo é sincronizado e todos os threads verão o novo valor. (sem um custo permanente nos sites de leitura, pode ser obsoleto ou incorrer em um custo único à medida que os caches são sincronizados, mas depois disso tudo volta ao campo normal até a próxima gravação).

Existe tal construção em Java ou em geral? Neste ponto, não posso deixar de pensar que, se algo assim existisse, ele já teria sido incorporado aos pacotes atômicos pelas pessoas mais inteligentes que os mantinham. (A leitura e a gravação desproporcionalmente frequentes podem não ter sido um caso para cuidar?) Então, talvez haja algo errado em meu pensamento e essa construção não seja possível?

Eu já vi alguns exemplos de código usar 'volátil' para uma finalidade semelhante, explorando o que acontece antes do contrato. Há um campo de sincronização separado, por exemplo:

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

e na escrita do tópico / site:

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

Não sei se isso funciona, e alguns argumentam que isso funciona apenas na arquitetura x86. Depois de ler as seções relacionadas no JMS, acho que é garantido que funcione apenas se essa gravação volátil estiver associada a uma leitura volátil dos threads que precisam ver o novo valor de myRef. (Portanto, não se livre da leitura volátil).

Voltando à minha pergunta original; Isso é possível em tudo? É possível em Java? É possível em uma das novas APIs no Java 9 VarHandles?

sydnal
fonte
1
Para mim, parece que você está bem no território em que precisa escrever e executar alguns benchmarks reais simulando suas cargas de trabalho.
NPE
1
O JMM afirma que, se o encadeamento do gravador sync += 1;e o encadeamento do leitor lerem o syncvalor, eles também verão a myRefatualização. Como você só precisa que os leitores vejam a atualização eventualmente , poderá usar isso como vantagem para ler a sincronização apenas a cada milésima iteração do encadeamento do leitor, ou algo semelhante. Mas você pode fazer um truque semelhante com volatile, demasiado - apenas cache o myRefcampo nos leitores para 1000 iterações, em seguida, lê-lo novamente usando volátil ...
Petr Janecek
@ PetrJaneček Mas ele não precisa sincronizar o acesso à variável do contador que é compartilhada entre os threads? Isso não será um gargalo? Na minha opinião, isso seria ainda mais caro.
Ravindra Ranwala
@RavindraRanwala Todo leitor terá seu próprio contador, se você quiser contar até 1000 iterações. Se você quis dizer o synccampo, não, os leitores não tocariam no synccampo em todas as iterações, eles fariam isso oportunisticamente, quando desejassem verificar se houve uma atualização. Dito isto, uma solução mais simples seria para armazenar em cache o myRefpara um 1000 rounds, então relê-lo ...
Petr Janecek
@ PetrJaneček obrigado, eu pensei nisso como uma possível solução. Mas estou pensando se isso é possível usando uma implementação genérica e sólida.
sydnal

Respostas:

2

Então, basicamente, você quer a semântica de a volatilesem o custo de tempo de execução.

Eu não acho que isso é possível.

O problema é que o custo de tempo de execução volatileé devido às instruções que implementam as barreiras de memória no gravador e no código do leitor. Se você "otimizar" o leitor, livrando-se de sua barreira de memória, não terá mais garantia de que o leitor verá o novo valor "raramente escrito" quando ele for realmente escrito.

FWIW, algumas versões da sun.misc.Unsafeclasse fornecem explícita loadFence, storeFenceefullFence métodos mas não acho que usá-los trará nenhum benefício de desempenho sobre o uso de a volatile.


Hipoteticamente ...

o que você deseja é que um processador em um sistema com vários processadores seja capaz de informar todos os outros processadores:

"Ei! O que você estiver fazendo, invalide seu cache de memória para o endereço XYZ e faça-o agora."

Infelizmente, os ISAs modernos não suportam isso.

Na prática, cada processador controla seu próprio cache.

Stephen C
fonte
Entendo, essa parte hipotética da sua resposta é o que eu procurava. Obrigado.
sydnal
0

Não tenho certeza se isso está correto, mas posso resolver isso usando uma fila.

Crie uma classe que agrupe um atributo ArrayBlockingQueue. A classe possui um método de atualização e um método de leitura. O método de atualização lança o novo valor na fila e remove todos os valores, exceto o último valor. O método read retorna o resultado de uma operação de espiada na fila, isto é, lê, mas não remove. Os encadeamentos que espreitam o elemento na frente da fila fazem isso sem impedimentos. Os encadeamentos que atualizam a fila fazem isso de forma limpa.

djhallx
fonte
0
  • Você pode usar o ReentrantReadWriteLockque é projetado para poucas gravações e muitas leituras.
  • Você pode usar o StampedLockque é projetado para o mesmo caso de poucas gravações e muitas leituras, mas também é possível tentar otimizações. Exemplo:

    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;
    }
  • Torne seu estado imutável e permita modificações controladas apenas pela clonagem do objeto existente e alterando apenas as propriedades necessárias por meio do construtor. Depois que o novo estado é construído, você o atribui à referência que está sendo lida pelos vários threads de leitura. Dessa forma, as threads de leitura incorrem em custo zero .

diginoise
fonte
se você sentir como downvoting, indique por que, para que o autor ea comunidade pode aprender
diginoise
Tornar imutável não é possível no meu cenário. E ficaria bastante surpreso se o estojo de trava carimbado custasse menos do que uma simples leitura volátil. No entanto, vou tentar, obrigado.
sydnal
0

X86 fornece TSO; você obtém cercas [LoadLoad] [LoadStore] [StoreStore] gratuitamente.

Uma leitura volátil requer semântica de liberação.

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

E como você pode ver, isso já é fornecido pelo X86 gratuitamente.

No seu caso, a maioria das chamadas é de leitura e o cacheline já estará no cache local.

Há um preço a pagar nas otimizações no nível do compilador, mas no nível do hardware, uma leitura volátil é tão cara quanto uma leitura regular.

Por outro lado, a gravação volátil é mais cara porque requer um [StoreLoad] para garantir consistência sequencial (na JVM, isso é feito usando um lock addl %(rsp),0 ou um MFENCE). Como as gravações raramente ocorrem na sua situação, isso não é um problema.

Eu teria cuidado com as otimizações nesse nível, porque é muito fácil tornar o código mais complexo do que o necessário. É melhor orientar seus esforços de desenvolvimento por alguns benchmarks, por exemplo, usando JMH e, de preferência, testá-lo em hardware real. Também poderia haver outras criaturas desagradáveis ​​escondidas como compartilhamento falso.

pveentjer
fonte