não presença de exclusão mútua ajuda se livrar da palavra-chave volátil?
-
06-07-2019 - |
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.
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 ??h2>
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).