Usando C/Pthreads:as variáveis ​​​​compartilhadas precisam ser voláteis?

StackOverflow https://stackoverflow.com/questions/78172

  •  09-06-2019
  •  | 
  •  

Pergunta

Na linguagem de programação C e Pthreads como biblioteca de threading;variáveis/estruturas compartilhadas entre threads precisam ser declaradas como voláteis?Supondo que eles possam estar protegidos por uma fechadura ou não (talvez barreiras).

O padrão POSIX pthread tem alguma palavra a dizer sobre isso, depende do compilador ou nenhum dos dois?

Edite para adicionar:Obrigado pelas ótimas respostas.Mas e se você estiver não usando fechaduras;e se você estiver usando barreiras por exemplo?Ou código que usa primitivos como comparar e trocar modificar direta e atomicamente uma variável compartilhada...

Foi útil?

Solução

Acho que uma propriedade muito importante do volátil é que ele faz com que a variável seja gravada na memória quando modificada e relida da memória cada vez que for acessada.As outras respostas aqui misturam volátil e sincronização, e fica claro, a partir de algumas outras respostas além desta, que volátil NÃO é uma primitiva de sincronização (crédito onde o crédito é devido).

Mas, a menos que você use volátil, o compilador estará livre para armazenar em cache os dados compartilhados em um registro por qualquer período de tempo...se você deseja que seus dados sejam gravados de forma previsível na memória real e não apenas armazenados em cache em um registro pelo compilador a seu critério, você precisará marcá-los como voláteis.Alternativamente, se você acessar os dados compartilhados apenas depois de deixar uma função modificando-os, você pode ficar bem.Mas eu sugeriria não confiar na sorte cega para garantir que os valores sejam gravados de volta dos registradores para a memória.

Especialmente em máquinas ricas em registros (ou seja, não x86), as variáveis ​​podem permanecer por longos períodos nos registros, e um bom compilador pode armazenar em cache até mesmo partes de estruturas ou estruturas inteiras nos registros.Portanto, você deve usar volátil, mas para desempenho, também copiar valores para variáveis ​​locais para cálculo e depois fazer um write-back explícito.Essencialmente, usar o volátil com eficiência significa pensar um pouco no armazenamento de carga em seu código C.

Em qualquer caso, você definitivamente precisa usar algum tipo de mecanismo de sincronização fornecido no nível do sistema operacional para criar um programa correto.

Para um exemplo da fraqueza do volátil, veja meu exemplo de algoritmo de Decker em http://jakob.engbloms.se/archives/65, o que prova muito bem que volátil não funciona para sincronizar.

Outras dicas

Contanto que você esteja usando bloqueios para controlar o acesso à variável, você não precisa de voláteis nela.Na verdade, se você estiver colocando volátil em alguma variável provavelmente já está errado.

https://software.intel.com/en-us/blogs/2007/11/30/volatile-almost-useless-for-multi-threaded-programming/

A resposta é absolutamente e inequivocamente NÃO.Você não precisa usar 'volátil' além das primitivas de sincronização adequadas.Tudo o que precisa ser feito é feito por essas primitivas.

O uso de 'volátil' não é necessário nem suficiente.Não é necessário porque as primitivas de sincronização adequadas são suficientes.Não é suficiente porque desativa apenas algumas otimizações, não todas aquelas que podem incomodar você.Por exemplo, não garante atomicidade ou visibilidade em outra CPU.

Mas, a menos que você use volátil, o compilador estará livre para armazenar em cache os dados compartilhados em um registro por qualquer período de tempo...se você deseja que seus dados sejam gravados de forma previsível na memória real e não apenas armazenados em cache em um registro pelo compilador a seu critério, você precisará marcá-los como voláteis.Alternativamente, se você acessar os dados compartilhados apenas depois de deixar uma função modificando-os, você pode ficar bem.Mas eu sugeriria não confiar na sorte cega para garantir que os valores sejam gravados de volta dos registradores para a memória.

Certo, mas mesmo se você usar volátil, a CPU estará livre para armazenar em cache os dados compartilhados em um buffer de postagem de gravação por qualquer período de tempo.O conjunto de otimizações que podem afetar você não é exatamente o mesmo que o conjunto de otimizações que o 'volátil' desativa.Então, se você usar 'volátil', você são confiando na sorte cega.

Por outro lado, se você usar primitivas de sincronização com semântica multithread definida, terá a garantia de que tudo funcionará.Além disso, você não sofre o enorme impacto no desempenho de 'volátil'.Então por que não fazer as coisas dessa maneira?

Existe uma noção generalizada de que a palavra-chave volátil é boa para programação multithread.

Hans Boehm aponta que existem apenas três usos portáteis para volátil:

  • volátil pode ser usado para marcar variáveis ​​locais no mesmo escopo de um setjmp cujo valor deve ser preservado em um longjmp.Não está claro qual fração de tais usos seria retardada, uma vez que as restrições de atomicidade e ordenação não têm efeito se não houver maneira de compartilhar a variável local em questão.(Não está claro até que fração de tais usos seria retardada pela exigência de que todas as variáveis ​​fossem preservadas em um longjmp, mas isso é um assunto separado e não é considerado aqui.)
  • volátil pode ser usado quando as variáveis ​​​​podem ser "modificadas externamente", mas a modificação na verdade é acionada de forma síncrona pelo próprio thread, por exemplo.porque a memória subjacente é mapeada em vários locais.
  • A volátil sigatom_t pode ser usado para se comunicar com um manipulador de sinal no mesmo thread, de forma restrita.Poderíamos considerar enfraquecer os requisitos para o caso sigatom_t, mas isso parece um tanto contra-intuitivo.

Se você estiver multithreading por uma questão de velocidade, desacelerar o código definitivamente não é o que você deseja.Para programação multithread, há dois problemas principais que muitas vezes se pensa erroneamente que o volátil aborda:

  • atomicidade
  • consistência de memória, ou sejaa ordem das operações de um thread conforme visto por outro thread.

Vamos lidar com (1) primeiro.Volatile não garante leituras ou gravações atômicas.Por exemplo, uma leitura ou gravação volátil de uma estrutura de 129 bits não será atômica na maioria dos hardwares modernos.Uma leitura ou gravação volátil de um int de 32 bits é atômica na maioria dos hardwares modernos, mas volátil não tem nada a ver com isso.Provavelmente seria atômico sem o volátil.A atomicidade fica por conta do compilador.Não há nada nos padrões C ou C++ que diga que deve ser atômico.

Agora considere a questão (2).Às vezes, os programadores pensam em volátil como o desligamento da otimização de acessos voláteis.Isso é em grande parte verdade na prática.Mas são apenas os acessos voláteis, não os não voláteis.Considere este fragmento:

 volatile int Ready;       

    int Message[100];      

    void foo( int i ) {      

        Message[i/10] = 42;      

        Ready = 1;      

    }

Ele está tentando fazer algo muito razoável na programação multithread:escreva uma mensagem e envie-a para outro tópico.O outro thread esperará até que Ready se torne diferente de zero e então lerá Message.Tente compilar isso com "gcc -O2 -S" usando gcc 4.0 ou icc.Ambos farão o armazenamento em Ready primeiro, para que possa ser sobreposto ao cálculo de i/10.A reordenação não é um bug do compilador.É um otimizador agressivo fazendo seu trabalho.

Você pode pensar que a solução é marcar todas as suas referências de memória como voláteis.Isso é simplesmente bobo.Como dizem as citações anteriores, isso apenas tornará seu código mais lento.Pior ainda, pode não resolver o problema.Mesmo que o compilador não reordene as referências, o hardware poderá fazê-lo.Neste exemplo, o hardware x86 não irá reordená-lo.Nem um processador Itanium(TM), porque os compiladores Itanium inserem barreiras de memória para armazenamentos voláteis.Essa é uma extensão inteligente do Itanium.Mas chips como o Power(TM) serão reordenados.O que você realmente precisa para fazer o pedido é cercas de memória, também chamado barreiras de memória.Um limite de memória evita a reordenação de operações de memória através do limite ou, em alguns casos, evita o reordenamento em uma direção. Volátil não tem nada a ver com limites de memória.

Então, qual é a solução para programação multithread?Use uma biblioteca ou extensão de linguagem que implemente a semântica atômica e de cerca.Quando usadas conforme pretendido, as operações na biblioteca inserirão as cercas corretas.Alguns exemplos:

  • Tópicos POSIX
  • Threads do Windows(TM)
  • OpenMP
  • A ser definido

Baseado em artigo de Arch Robison (Intel)

Na minha experiência, não;você só precisa fazer mutex adequadamente ao gravar esses valores ou estruturar seu programa de forma que os threads parem antes de precisarem acessar dados que dependem das ações de outro thread.Meu projeto, x264, usa esse método;threads compartilham uma enorme quantidade de dados, mas a grande maioria deles não precisa de mutexes porque é somente leitura ou um thread aguardará que os dados sejam disponibilizados e finalizados antes de precisar acessá-los.

Agora, se você tiver muitos threads fortemente intercalados em suas operações (eles dependem da saída uns dos outros em um nível muito refinado), isso pode ser muito mais difícil - na verdade, nesse caso, eu considere revisitar o modelo de threading para ver se isso pode ser feito de forma mais limpa com mais separação entre threads.

NÃO.

Volatile só é necessário ao ler um local de memória que pode mudar independentemente dos comandos de leitura/gravação da CPU.Na situação de threading, a CPU tem controle total de leitura/gravação na memória para cada thread, portanto o compilador pode assumir que a memória é coerente e otimiza as instruções da CPU para reduzir o acesso desnecessário à memória.

O uso principal para volatile é para acessar E/S mapeada em memória.Neste caso, o dispositivo subjacente pode alterar o valor de um local de memória independentemente da CPU.Se você não usar volatile sob esta condição, a CPU pode usar um valor de memória previamente armazenado em cache, em vez de ler o valor recém-atualizado.

Volátil só seria útil se você não precisasse de nenhum atraso entre o momento em que um thread escreve algo e outro thread o lê.Sem algum tipo de bloqueio, porém, você não tem ideia do quando o outro thread gravou os dados, apenas que é o valor mais recente possível.

Para valores simples (int e float em seus vários tamanhos), um mutex pode ser um exagero se você não precisar de um ponto de sincronização explícito.Se você não usar algum tipo de mutex ou bloqueio, deverá declarar a variável volátil.Se você usar um mutex, está tudo pronto.

Para tipos complicados, você deve usar um mutex.As operações neles não são atômicas, então você pode ler uma versão parcialmente alterada sem mutex.

Volátil significa que temos que ir à memória para obter ou definir esse valor.Se você não definir volátil, o código compilado poderá armazenar os dados em um registro por um longo tempo.

O que isso significa é que você deve marcar variáveis ​​​​que você compartilha entre threads como voláteis para que você não tenha situações em que um thread comece a modificar o valor, mas não grave seu resultado antes que um segundo thread apareça e tente ler o valor .

Volátil é uma dica do compilador que desativa certas otimizações.O assembly de saída do compilador poderia ter sido seguro sem ele, mas você deve sempre usá-lo para valores compartilhados.

Isso é especialmente importante se você NÃO estiver usando objetos caros de sincronização de threads fornecidos pelo seu sistema - você pode, por exemplo, ter uma estrutura de dados onde pode mantê-la válida com uma série de alterações atômicas.Muitas pilhas que não alocam memória são exemplos de tais estruturas de dados, porque você pode adicionar um valor à pilha e, em seguida, mover o ponteiro final ou remover um valor da pilha após mover o ponteiro final.Ao implementar tal estrutura, volátil torna-se crucial para garantir que suas instruções atômicas sejam realmente atômicas.

A razão subjacente é que a semântica da linguagem C é baseada em um máquina abstrata de thread único.E o compilador tem o direito de transformar o programa, desde que os “comportamentos observáveis” do programa na máquina abstrata permaneçam inalterados.Ele pode mesclar acessos à memória adjacentes ou sobrepostos, refazer um acesso à memória várias vezes (ao derramar o registro, por exemplo) ou simplesmente descartar um acesso à memória, se considerar o comportamento do programa, quando executado em um único tópico, não muda.Portanto, como você pode suspeitar, os comportamentos fazer mude se o programa realmente estiver sendo executado de maneira multithread.

Como Paul Mckenney apontou em um famoso Documento do kernel Linux:

Ele _must_not_ é assumido que o compilador fará o que você deseja com referências de memória que não são protegidas por read_once () e write_once ().Sem eles, o compilador está dentro de seus direitos de fazer todos os tipos de transformações "criativas", cobertas na seção de barreira do compilador.

READ_ONCE() e WRITE_ONCE() são definidos como conversões voláteis em variáveis ​​referenciadas.Por isso:

int y;
int x = READ_ONCE(y);

é equivalente a:

int y;
int x = *(volatile int *)&y;

Então, a menos que você faça um acesso ‘volátil’, você não tem certeza de que o acesso acontecerá exatamente uma vez, não importa qual mecanismo de sincronização você esteja usando.Chamar uma função externa (pthread_mutex_lock por exemplo) pode forçar o compilador a acessar a memória para variáveis ​​globais.Mas isso acontece apenas quando o compilador não consegue descobrir se a função externa altera essas variáveis ​​globais ou não.Compiladores modernos que empregam análise sofisticada entre procedimentos e otimização de tempo de link tornam esse truque simplesmente inútil.

Em resumo, você deve marcar variáveis ​​compartilhadas por vários threads como voláteis ou acessá-las usando conversões voláteis.


Como Paul McKenney também apontou:

Tenho visto o brilho em seus olhos quando discutem técnicas de otimização que você não gostaria que seus filhos conhecessem!


Mas veja o que acontece C11/C++11.

Eu não entendo.Como a sincronização de primitivas força o compilador a recarregar o valor de uma variável?Por que não usaria apenas a cópia mais recente que já possui?

Volátil significa que a variável é atualizada fora do escopo do código e, portanto, o compilador não pode presumir que conhece o valor atual dela.Até as barreiras de memória são inúteis, pois o compilador, que ignora as barreiras de memória (certo?), ainda pode usar um valor armazenado em cache.

Obviamente, algumas pessoas estão assumindo que o compilador trata as chamadas de sincronização como barreiras de memória."Casey" está assumindo que há exatamente uma CPU.

Se as primitivas de sincronização forem funções externas e os símbolos em questão forem visíveis fora da unidade de compilação (nomes globais, ponteiro exportado, função exportada que pode modificá-los), então o compilador irá tratá-los - ou qualquer outra chamada de função externa - como um cerca de memória em relação a todos os objetos visíveis externamente.

Caso contrário, você estará sozinho.E volátil pode ser a melhor ferramenta disponível para fazer o compilador produzir código correto e rápido.Geralmente, porém, não será portátil, quando você precisar de volátil e o que ele realmente faz por você depende muito do sistema e do compilador.

Não.

Primeiro, volatile não é necessário.Existem inúmeras outras operações que fornecem semântica multithread garantida que não usa volatile.Isso inclui operações atômicas, mutexes e assim por diante.

Segundo, volatile não é suficiente.O padrão C não fornece nenhuma garantia sobre o comportamento multithread para variáveis ​​declaradas volatile.

Portanto, não sendo necessário nem suficiente, não faz muito sentido usá-lo.

Uma exceção seriam plataformas específicas (como o Visual Studio) onde há semântica multithread documentada.

Variáveis ​​compartilhadas entre threads devem ser declaradas 'voláteis'.Isso diz ao compilador que, quando um thread grava para essas variáveis, a gravação deve ser a memória (em oposição a um registro).

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