Pergunta

Eu tenho uma classe de bloqueio multi-R / W que mantém a ler, escrever e pendentes ler, pendente contadores de escrita. Um mutex protege-los de vários segmentos.

A minha pergunta é que ainda precisamos os contadores para ser declarado como tão volátil que o compilador não vai estragar tudo ao fazer a otimização.

Ou o compilador leva em conta que os contadores são guardados pela exclusão mútua.

Eu entendo que a exclusão mútua é um mecanismo de tempo de execução para para sincronização e palavra-chave "volátil" é uma indicação de tempo de compilação para o compilador para fazer a coisa certa ao fazer as otimizações.

Saudações, -Jay.

Foi útil?

Solução

Existem 2 itens basicamente não relacionados aqui, que estão sempre confusos.

  • volátil
  • fios, fechaduras, barreiras de memória, etc.

volátil é usado para dizer ao compilador para código de produto para ler a variável de memória, não a partir de um registo. E para não reordenar o código ao redor. Em geral, não para otimizar ou tomar 'atalhos'.

barreiras de memória (fornecido por semáforos, fechaduras, etc), como citado de Herb Sutter em outra resposta, são para prevenir a CPU de reordenação pedidos de leitura / memória de gravação, independentemente de como o compilador disse para fazê-lo. ou seja, não optimize, não tome atalhos -. ao nível CPU

similares, mas na verdade coisas muito diferentes.

No seu caso, e na maioria dos casos de bloqueio, a razão pela qual volátil não é necessário, é por causa de chamadas de função que está sendo feito para o bem de bloqueio. ou seja:

chamadas de funções normais que afetam otimizações:

external void library_func(); // from some external library

global int x;

int f()
{
   x = 2;
   library_func();
   return x; // x is reloaded because it may have changed
}

a menos que o compilador pode examinar library_func () e determinar que ele não toque x, ele vai x voltar a ler sobre o retorno. Isto é, mesmo sem volátil.

Enfiar:

int f(SomeObject & obj)
{
   int temp1;
   int temp2;
   int temp3;

   int temp1 = obj.x;

   lock(obj.mutex); // really should use RAII
      temp2 = obj.x;
      temp3 = obj.x;
   unlock(obj.mutex);

   return temp;
}

Depois de ler obj.x para temp1, o compilador vai voltar a ler obj.x para temp2 - não por causa da magia de bloqueios - mas porque ele não tem certeza se lock () modificado obj. Você provavelmente poderia definir bandeiras do compilador para agressivamente optimize (sem apelido, etc) e, portanto, não re-ler x, mas, em seguida, um monte de seu código provavelmente começar a falhar.

Para TEMP3, o compilador (espero) não vai re-leitura obj.x. Se por algum motivo obj.x poderia mudar entre temp2 e TEMP3, então você usaria volátil (e seu bloqueio seria quebrado / inútil).

Por último, se o seu bloqueio () / desbloqueio () funções foram de alguma forma inline, talvez o compilador poderia avaliar o código e ver que obj.x não se alterou. Mas eu garanto uma de duas coisas aqui: - o código embutido, eventualmente, chama alguns função de bloqueio de nível de sistema operacional (avaliação evitando assim) ou -. Você chama algumas instruções de barreira de memória asm (ou seja, que são enrolados em funções inline como __InterlockedCompareExchange) que o compilador irá reconhecer e, assim, evitar reordenação

EDIT: P. S. Eu esqueci de mencionar - para coisas pthreads, alguns compiladores são marcados como "compatível com POSIX", que significa, entre outras coisas, que eles irão reconhecer as funções pthread_ e não fazer otimizações ruins em torno deles. ou seja, mesmo que o C ++ padrão não menciona tópicos ainda, aqueles compiladores fazer (pelo menos minimamente).

Então, resposta curta

Você não precisa volátil.

Outras dicas

De artigo de Herb Sutter "uso crítico seções (de preferência Locks) para eliminar raças" ( http: // www .ddj.com / CPP / 201804238 ):

Assim, para uma transformação reordenamento para ser válido, deve respeitar as seções críticas do programa, obedecendo a uma regra fundamental de seções críticas: Código não pode sair de uma seção crítica. (É sempre bom para código para mover pol.) Nós impor essa regra de ouro, exigindo semântica cerca de sentido único simétricas para o início eo fim de qualquer seção crítica, ilustrado pelas setas na Figura 1:

  • Entrando em uma seção crítica é uma operação adquirir, ou uma cerca adquirir implícito: Código nunca pode atravessar a cerca para cima, isto é, movimento a partir de um local original após a cerca para executar antes que o cerca. Código que aparece antes da cerca, a fim de código-fonte, no entanto, pode feliz atravessar o muro baixo para executar mais tarde.
  • Sair de uma seção crítica é uma operação de libertação, ou uma cerca liberação implícito: Esta é apenas a exigência inversa que o código não pode atravessar o muro baixo, única cima. Ele garante que qualquer outro segmento que vê a gravação versão final também vai ver todas as gravações antes.

Assim, para um compilador para produzir código correto para a plataforma de destino, quando uma seção crítica está inserido e que acabou (e na seção crítica termo é usado nele é sentido genérico, não necessariamente no sentido Win32 de algo protegido por uma estrutura CRITICAL_SECTION - a secção crítica pode ser protegido por outros objectos de sincronização) a semântica adquirir e libertação correcção deve ser seguido. Então, você não deve ter para marcar as variáveis ??compartilhadas como volátil, enquanto eles são acessados ??somente dentro de seções críticas protegidas.

volátil é usada para informar o otimizador para sempre carregar o valor atual do local, ao invés de carregá-lo em um registo e assumir que isso não vai mudar. Esta é mais valioso quando se trabalha com posições de memória ou locais de duas portas que podem ser atualizados em tempo real a partir de fontes externas para o segmento.

O mutex é um mecanismo OS em tempo de execução que o compilador realmente não sabe nada sobre - para que o otimizador não levar isso em conta. Ele irá impedir mais do que um fio de acessar os contadores de uma só vez, mas os valores desses contadores ainda estão sujeitos a alterações, mesmo quando o mutex está em vigor.

Então, você está marcando o vars volátil, porque eles podem ser modificados externamente, e não porque eles estão dentro de um guarda mutex.

Mantenha-volátil.

Enquanto isto pode depender da biblioteca de threading que você está usando, o meu entendimento é que qualquer biblioteca decente vai não requer o uso de volatile.

Em Pthreads, por exemplo , o uso de um mutex irá garantir que seus dados fica comprometida com a memória corretamente.

EDIT: Tenho a honra de endossar resposta de Tony como sendo melhor do que a minha.

Você ainda precisa a palavra-chave "volátil".

Os semáforos impedir que os contadores de acesso simultâneo.

"volátil" indica o compilador para realmente usar o contador em vez de cache-lo em um registo CPU (que não faria ser atualizado pela thread concorrente).

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top