Por que o volátil não é considerado útil na programação C ou C ++ multithread?


165

Como demonstrado nesta resposta que postei recentemente, pareço estar confuso sobre a utilidade (ou a falta dela) volatileem contextos de programação multithread.

Meu entendimento é o seguinte: sempre que uma variável pode ser alterada fora do fluxo de controle de um pedaço de código que a acessa, essa variável deve ser declarada volatile. Manipuladores de sinal, registradores de E / S e variáveis ​​modificadas por outro encadeamento constituem todas essas situações.

Portanto, se você tem um int global fooe fooé lido por um thread e definido atomicamente por outro (provavelmente usando uma instrução de máquina apropriada), o thread de leitura vê essa situação da mesma maneira que vê uma variável ajustada por um manipulador de sinal ou modificado por uma condição de hardware externo e, portanto, foodeve ser declarado volatile(ou, para situações multithread, acessado com carga protegida pela memória, o que provavelmente é a melhor solução).

Como e onde estou errado?


7
Tudo o que o volátil faz é dizer que o compilador não deve armazenar em cache o acesso a uma variável volátil. Não diz nada sobre serializar esse acesso. Isso foi discutido aqui. Não sei quantas vezes e não acho que essa pergunta adicione algo a essas discussões.

4
E mais uma vez, uma pergunta que não merece, e já foi feita aqui muitas vezes antes de ser votada. Você pode parar de fazer isso?

14
@neil Procurei outras perguntas e encontrei uma, mas qualquer explicação existente que vi de alguma forma não desencadeou o que eu precisava para realmente entender por que estava errado. Esta pergunta provocou uma resposta desse tipo.
Michael Ekstrand 21/03

1
Para um grande estudo em profundidade sobre o que CPUs fazer com os dados (via seus caches) confira: rdrop.com/users/paulmck/scalability/paper/whymb.2010.06.07c.pdf
Sassafras_wot

1
@curiousguy É o que eu quis dizer com "não é o caso em C", onde ele pode ser usado para gravar em registros de hardware etc., e não é usado para multithreading, como é comumente usado em Java.
Monstieur

Respostas:


213

O problema volatileem um contexto multithread é que ele não fornece todas as garantias que precisamos. Ele possui algumas propriedades de que precisamos, mas não todas, por isso não podemos confiar volatile sozinhas .

No entanto, as primitivas que precisaríamos usar para as propriedades restantes também fornecem as que são fornecidas volatile, portanto, é efetivamente desnecessário.

Para acessos seguros para threads a dados compartilhados, precisamos de uma garantia de que:

  • a leitura / gravação realmente acontece (que o compilador não armazena apenas o valor em um registro e adia a atualização da memória principal até muito mais tarde)
  • que nenhuma reordenação ocorre. Suponha que usamos uma volatilevariável como sinalizador para indicar se alguns dados estão prontos para serem lidos ou não. Em nosso código, simplesmente definimos o sinalizador após a preparação dos dados, para que tudo fique bem. Mas e se as instruções forem reordenadas para que o sinalizador seja definido primeiro ?

volatilegarante o primeiro ponto. Também garante que nenhuma reordenação ocorra entre diferentes leituras / gravações voláteis . Todos volatileos acessos à memória ocorrerão na ordem em que foram especificados. É tudo o que precisamos para o que volatilese destina: manipular registros de E / S ou hardware mapeado na memória, mas isso não nos ajuda no código multithread, onde o volatileobjeto geralmente é usado apenas para sincronizar o acesso a dados não voláteis. Esses acessos ainda podem ser reordenados em relação volatileàqueles.

A solução para impedir a reordenação é usar uma barreira de memória , que indica ao compilador e à CPU que nenhum acesso à memória pode ser reordenado nesse ponto . A colocação de tais barreiras em torno do nosso acesso variável volátil garante que mesmo os acessos não voláteis não sejam reordenados no volátil, permitindo escrever código com segurança para threads.

No entanto, as barreiras de memória também garantem que todas as leituras / gravações pendentes sejam executadas quando a barreira for atingida, portanto, efetivamente nos fornece tudo o que precisamos, tornando volatiledesnecessário. Podemos apenas remover volatilecompletamente o qualificador.

Desde o C ++ 11, as variáveis ​​atômicas ( std::atomic<T>) nos dão todas as garantias relevantes.


5
@jbcreix: De que "você" está perguntando? Barreiras voláteis ou de memória? De qualquer forma, a resposta é praticamente a mesma. Ambos precisam trabalhar no nível do compilador e da CPU, pois descrevem o comportamento observável do programa - para garantir que a CPU não reordene tudo, alterando o comportamento que eles garantem. Mas atualmente você não pode escrever a sincronização de encadeamento portátil, porque as barreiras de memória não fazem parte do C ++ padrão (portanto, não são portáteis) e volatilenão são fortes o suficiente para serem úteis.
jalf

4
Um exemplo do MSDN faz isso e afirma que as instruções não podem ser reordenadas após um acesso volátil: msdn.microsoft.com/en-us/library/12a04hfd(v=vs.80).aspx
OJW

27
@OJW: Mas o compilador da Microsoft redefine volatileser uma barreira de memória completa (impedindo a reordenação). Isso não faz parte do padrão, portanto você não pode confiar nesse comportamento no código portátil.
jalf

4
@ Skizz: não, é aí que entra a parte "mágica do compilador" da equação. Uma barreira de memória deve ser entendida tanto pela CPU quanto pelo compilador. Se o compilador entende a semântica de uma barreira de memória, ele sabe evitar truques como esse (além de reordenar as leituras / gravações na barreira). E, felizmente, o compilador faz compreender a semântica de uma barreira de memória, assim, no final, tudo funciona. :)
jalf

13
@ Skizz: Os próprios threads sempre são uma extensão dependente da plataforma antes do C ++ 11 e C11. Que eu saiba, todo ambiente C e C ++ que fornece uma extensão de encadeamento também fornece uma extensão de "barreira de memória". Independentemente disso, volatileé sempre inútil para programação multithread. (Exceto no Visual Studio, onde volátil é a extensão da barreira da memória.)
Nemo

49

Você também pode considerar isso na documentação do kernel do Linux .

Os programadores C geralmente consideram volátil o significado de que a variável pode ser alterada fora do encadeamento atual de execução; como resultado, às vezes são tentados a usá-lo no código do kernel quando estruturas de dados compartilhadas estão sendo usadas. Em outras palavras, eles são conhecidos por tratar tipos voláteis como uma espécie de variável atômica fácil, o que não é. O uso de volátil no código do kernel quase nunca é correto; este documento descreve o porquê.

O ponto principal a ser entendido em relação ao volátil é que seu objetivo é suprimir a otimização, que quase nunca é o que realmente se deseja fazer. No kernel, é necessário proteger as estruturas de dados compartilhadas contra acesso simultâneo indesejado, o que é uma tarefa muito diferente. O processo de proteção contra concorrência indesejada também evitará quase todos os problemas relacionados à otimização de maneira mais eficiente.

Como voláteis, as primitivas do kernel que tornam seguro o acesso simultâneo aos dados (spinlocks, mutexes, barreiras de memória etc.) são projetadas para impedir a otimização indesejada. Se eles estiverem sendo usados ​​corretamente, não haverá necessidade de usar voláteis também. Se o volátil ainda for necessário, há quase certamente um bug no código em algum lugar. No código do kernel corretamente escrito, o volátil pode servir apenas para retardar as coisas.

Considere um bloco típico de código do kernel:

spin_lock(&the_lock);
do_something_on(&shared_data);
do_something_else_with(&shared_data);
spin_unlock(&the_lock);

Se todo o código seguir as regras de bloqueio, o valor de shared_data não poderá ser alterado inesperadamente enquanto o bloqueio for mantido. Qualquer outro código que queira jogar com esses dados estará aguardando o bloqueio. As primitivas spinlock agem como barreiras de memória - elas foram escritas explicitamente para isso - o que significa que o acesso a dados não será otimizado entre eles. Portanto, o compilador pode pensar que sabe o que haverá em shared_data, mas a chamada spin_lock (), uma vez que atua como uma barreira de memória, forçará a esquecer qualquer coisa que ele saiba. Não haverá problemas de otimização no acesso a esses dados.

Se shared_data fosse declarado volátil, o bloqueio ainda seria necessário. Mas o compilador também seria impedido de otimizar o acesso a shared_data na seção crítica, quando sabemos que ninguém mais pode trabalhar com ele. Enquanto o bloqueio é mantido, shared_data não é volátil. Ao lidar com dados compartilhados, o bloqueio adequado torna volátil desnecessário - e potencialmente prejudicial.

A classe de armazenamento volátil foi originalmente criada para registros de E / S mapeados na memória. Dentro do kernel, os acessos ao registro também devem ser protegidos por bloqueios, mas também não se deseja que o compilador "otimize" os acessos ao registro em uma seção crítica. Mas, dentro do kernel, os acessos à memória de E / S são sempre feitos através das funções do acessador; acessar a memória de E / S diretamente através de ponteiros é desaprovado e não funciona em todas as arquiteturas. Esses acessadores são gravados para impedir a otimização indesejada; portanto, mais uma vez, a volatilidade é desnecessária.

Outra situação em que alguém pode ficar tentado a usar volátil é quando o processador está ocupado aguardando o valor de uma variável. A maneira correta de executar uma espera ocupada é:

while (my_variable != what_i_want)
    cpu_relax();

A chamada cpu_relax () pode diminuir o consumo de energia da CPU ou render a um processador gêmeo com hyperthread; isso também serve como barreira à memória; portanto, mais uma vez, volátil é desnecessário. Obviamente, a espera ocupada é geralmente um ato anti-social, para começar.

Ainda existem algumas situações raras em que volátil faz sentido no kernel:

  • As funções do acessador acima mencionadas podem ser voláteis em arquiteturas nas quais o acesso direto à memória de E / S funciona. Essencialmente, cada chamada do acessador se torna uma seção crítica por si só e garante que o acesso ocorra conforme o esperado pelo programador.

  • O código de montagem embutido que altera a memória, mas que não possui outros efeitos colaterais visíveis, corre o risco de ser excluído pelo GCC. Adicionar a palavra-chave volátil às instruções asm impedirá essa remoção.

  • A variável jiffies é especial, pois pode ter um valor diferente toda vez que é referenciada, mas pode ser lida sem nenhum bloqueio especial. Portanto, os instantes podem ser voláteis, mas a adição de outras variáveis ​​desse tipo é fortemente desaprovada. Jiffies é considerado uma questão de "legado estúpido" (palavras de Linus) a esse respeito; consertá-lo seria mais complicado do que vale a pena.

  • Ponteiros para estruturas de dados na memória coerente que podem ser modificados por dispositivos de E / S podem, às vezes, ser legitimamente voláteis. Um buffer de anel usado por um adaptador de rede, em que esse adaptador altera os ponteiros para indicar quais descritores foram processados, é um exemplo desse tipo de situação.

Para a maioria dos códigos, nenhuma das justificativas acima para volatilidade se aplica. Como resultado, é provável que o uso de volátil seja visto como um bug e trará um exame adicional ao código. Os desenvolvedores que são tentados a usar o volátil devem dar um passo atrás e pensar no que estão realmente tentando realizar.



1
O spin_lock () parece uma chamada de função regular. O que há de especial nisso, que o compilador tratará especialmente, para que o código gerado "esqueça" qualquer valor de dados compartilhados que foram lidos antes do spin_lock () e armazenados em um registro, para que o valor precise ser lido novamente no diretório do_something_on () após o spin_lock ()?
Syncopated

1
@underscore_d Meu argumento é que não posso dizer pelo nome da função spin_lock () que ele faz algo especial. Eu não sei o que há nele. Particularmente, não sei o que há na implementação que impede o compilador de otimizar as leituras subsequentes.
Syncopated 07/07

1
Syncopated tem um bom ponto. Isso significa essencialmente que o programador deve conhecer a implementação interna dessas "funções especiais" ou pelo menos estar muito bem informado sobre seu comportamento. Isso levanta questões adicionais, como - essas funções especiais são padronizadas e garantidas para funcionar da mesma maneira em todas as arquiteturas e todos os compiladores? Existe uma lista dessas funções disponíveis ou pelo menos existe uma convenção para usar comentários de código para sinalizar aos desenvolvedores que a função em questão protege o código contra a "otimização ausente"?
JustAMartin

1
@ Tuntable: Uma estática privada pode ser tocada por qualquer código, através de um ponteiro. E seu endereço está sendo usado. Talvez a análise do fluxo de dados seja capaz de provar que o ponteiro nunca escapa, mas esse é, em geral, um problema muito difícil, superlinear no tamanho do programa. Se você tem como garantir que não haja alias, mover o acesso através de um bloqueio de rotação deve estar ok. Mas se não houver apelidos, também não fará volatilesentido. Em todos os casos, o comportamento "chamar uma função cujo corpo não pode ser visto" estará correto.
Ben Voigt

11

Eu não acho que você esteja errado - volátil é necessário para garantir que o segmento A veja o valor mudar, se o valor for alterado por algo diferente do segmento A. Pelo que entendi, o volátil é basicamente uma maneira de dizer ao compilador "não armazene em cache essa variável em um registro; em vez disso, sempre leia / escreva na memória RAM em todos os acessos".

A confusão é que a volatilidade não é suficiente para implementar várias coisas. Em particular, os sistemas modernos usam vários níveis de armazenamento em cache, as CPUs modernas com vários núcleos fazem algumas otimizações sofisticadas em tempo de execução, e os compiladores modernos fazem algumas otimizações sofisticadas em tempo de compilação, e tudo isso pode resultar em vários efeitos colaterais aparecendo em diferentes pedido da ordem que você esperaria se apenas visse o código-fonte.

Tão volátil é bom, desde que você tenha em mente que as alterações 'observadas' na variável volátil podem não ocorrer no momento exato em que você pensa que elas ocorrerão. Especificamente, não tente usar variáveis ​​voláteis como uma maneira de sincronizar ou ordenar operações através de threads, porque isso não funcionará de maneira confiável.

Pessoalmente, meu principal (apenas?) Uso para o sinalizador volátil é como um booleano "pleaseGoAwayNow". Se eu tiver um thread de trabalho que faça loops continuamente, faça com que ele verifique o booleano volátil em cada iteração do loop e saia se o booleano for verdadeiro. O thread principal pode limpar com segurança o thread de trabalho, configurando o booleano como true e, em seguida, chamando pthread_join () para aguardar até que o thread de trabalho acabe.


2
Sua bandeira booleana provavelmente não é segura. Como você garante que o trabalhador conclua sua tarefa e que o sinalizador permaneça no escopo até ser lido (se for lido)? Esse é um trabalho para sinais. O volátil é bom para implementar spinlocks simples se nenhum mutex estiver envolvido, pois a segurança alias significa que o compilador assume mutex_lock(e todas as outras funções da biblioteca) podem alterar o estado da variável flag.
Potatoswatter 20/03/10

6
Obviamente, ele só funciona se a natureza da rotina do encadeamento do trabalhador for tal que seja garantido que verifique o booleano periodicamente. É garantido que o sinalizador bool volátil permaneça no escopo porque a sequência de desligamento do encadeamento sempre ocorre antes que o objeto que contém o booleano volátil seja destruído, e a sequência de desligamento do encadeamento chama pthread_join () após definir o bool. pthread_join () bloqueará até que o thread de trabalho desapareça. Os sinais têm seus próprios problemas, principalmente quando usados ​​em conjunto com o multithreading.
Jeremy Friesner 20/03/10

2
Não é garantido que o thread de trabalho conclua seu trabalho antes que o booleano seja verdadeiro - na verdade, ele quase certamente estará no meio de uma unidade de trabalho quando o bool estiver definido como true. Mas não importa quando o segmento de trabalho conclui sua unidade de trabalho, porque o segmento principal não fará nada além de bloquear dentro de pthread_join () até que o segmento de trabalho saia, em qualquer caso. Portanto, a sequência de desligamento é bem ordenada - o bool volátil (e quaisquer outros dados compartilhados) não será liberado até que pthread_join () retorne e pthread_join () não retorne até que o thread de trabalho acabe.
precisa saber é o seguinte

10
@ Jeremy, você está correto na prática, mas teoricamente ainda pode quebrar. Em um sistema de dois núcleos, um núcleo está constantemente executando seu encadeamento de trabalho. O outro núcleo define o bool como true. No entanto, não há garantia de que o núcleo do thread de trabalho jamais verá essa alteração, ou seja, nunca poderá parar, mesmo que verifique repetidamente o bool. Esse comportamento é permitido pelos modelos de memória c ++ 0x, java e c #. Na prática, isso nunca ocorreria, pois o encadeamento ocupado provavelmente inseria uma barreira de memória em algum lugar, após o que ocorrerá a alteração no bool.
Deft_code 22/03/10

4
Pegue um sistema POSIX, use política de agendamento em tempo real SCHED_FIFO, prioridade estática mais alta que outros processos / threads no sistema, núcleos suficientes, deve ser perfeitamente possível. No Linux, você pode especificar que o processo em tempo real possa usar 100% do tempo da CPU. Eles nunca mudarão de contexto se não houver um thread / processo de maior prioridade e nunca bloquearem por E / S. Mas o ponto é que o C / C ++ volatilenão se destina a impor a semântica adequada de compartilhamento / sincronização de dados. Acho que pesquisar casos especiais para provar que código incorreto talvez às vezes funcione é um exercício inútil.
Foof

7

volatileé útil (embora insuficiente) para implementar a construção básica de um mutex spinlock, mas depois que você tiver (ou algo superior), não precisará de outro volatile.

A maneira típica de programação multithread não é proteger todas as variáveis ​​compartilhadas no nível da máquina, mas introduzir variáveis ​​de guarda que orientam o fluxo do programa. Em vez de volatile bool my_shared_flag;você deveria ter

pthread_mutex_t flag_guard_mutex; // contains something volatile
bool my_shared_flag;

Isso não apenas encapsula a "parte difícil", é fundamentalmente necessário: C não inclui operações atômicas necessárias para implementar um mutex; ele só precisa volatiledar garantias extras sobre operações comuns .

Agora você tem algo parecido com isto:

pthread_mutex_lock( &flag_guard_mutex );
my_local_state = my_shared_flag; // critical section
pthread_mutex_unlock( &flag_guard_mutex );

pthread_mutex_lock( &flag_guard_mutex ); // may alter my_shared_flag
my_shared_flag = ! my_shared_flag; // critical section
pthread_mutex_unlock( &flag_guard_mutex );

my_shared_flag não precisa ser volátil, apesar de inatingível, porque

  1. Outro segmento tem acesso a ele.
  2. Significando que uma referência a ela deve ter sido feita em algum momento (com o &operador).
    • (Ou foi feita referência a uma estrutura de contenção)
  3. pthread_mutex_lock é uma função de biblioteca.
  4. Significando que o compilador não pode dizer se de pthread_mutex_lockalguma forma adquire essa referência.
  5. Significando que o compilador deve assumir que pthread_mutex_lockmodifica o sinalizador compartilhado !
  6. Portanto, a variável deve ser recarregada da memória. volatile, embora significativo nesse contexto, é estranho.

6

Sua compreensão está realmente errada.

A propriedade, que as variáveis ​​voláteis possuem, é "lê e grava nessa variável fazem parte do comportamento perceptível do programa". Isso significa que este programa funciona (com o hardware apropriado):

int volatile* reg=IO_MAPPED_REGISTER_ADDRESS;
*reg=1; // turn the fuel on
*reg=2; // ignition
*reg=3; // release
int x=*reg; // fire missiles

O problema é que essa não é a propriedade que queremos de algo seguro para threads.

Por exemplo, um contador seguro para threads seria apenas (código semelhante ao kernel do linux, não conheço o equivalente em c ++ 0x):

atomic_t counter;

...
atomic_inc(&counter);

Isso é atômico, sem uma barreira de memória. Você deve adicioná-los, se necessário. Adicionar volátil provavelmente não ajudaria, porque não relacionaria o acesso ao código próximo (por exemplo, à adição de um elemento à lista que o contador está contando). Certamente, você não precisa ver o contador incrementado fora do seu programa, e as otimizações ainda são desejáveis, por exemplo.

atomic_inc(&counter);
atomic_inc(&counter);

ainda pode ser otimizado para

atomically {
  counter+=2;
}

se o otimizador for inteligente o suficiente (não altera a semântica do código).


6

Para que seus dados sejam consistentes em um ambiente simultâneo, você precisa de duas condições para aplicar:

1) Atomicidade, isto é, se eu ler ou gravar alguns dados na memória, esses dados serão lidos / gravados em uma passagem e não poderão ser interrompidos ou contestados devido, por exemplo, a uma mudança de contexto

2) Consistência ou seja, a ordem de ops de leitura / gravação deve ser visto para ser o mesmo entre vários ambientes simultâneos - ser que threads, máquinas etc

volátil não se encaixa em nenhum dos itens acima - ou mais particularmente, o padrão c ou c ++ sobre como o comportamento dos materiais voláteis não inclui nenhum dos itens acima.

É ainda pior na prática, pois alguns compiladores (como o compilador Intel Itanium) tentam implementar algum elemento de comportamento seguro de acesso simultâneo (ou seja, garantindo cercas de memória), no entanto, não há consistência entre as implementações do compilador e, além disso, o padrão não exige isso. da implementação em primeiro lugar.

Marcar uma variável como volátil significa apenas que você está forçando o valor a ser liberado para e da memória a cada vez, o que, em muitos casos, apenas reduz a velocidade do seu código, pois você basicamente reduz o desempenho do cache.

c # e java AFAIK corrigem isso, tornando volátil a aderência a 1) e 2), no entanto, o mesmo não pode ser dito para os compiladores c / c ++, então basicamente faça isso como achar melhor.

Para uma discussão mais aprofundada (embora não imparcial) sobre o assunto, leia este


3
+1 - atomicidade garantida era outra parte do que estava faltando. Eu estava assumindo que o carregamento de um int é atômico, de modo que a prevenção volátil, impedindo a reordenação, forneceu a solução completa no lado da leitura. Eu acho que é uma suposição decente na maioria das arquiteturas, mas não é uma garantia.
Michael Ekstrand 21/03

Quando as leituras e gravações individuais na memória são interrompidas e não atômicas? Existe algum benefício?
batbrat

5

A FAQ do comp.programming.threads tem uma explicação clássica de Dave Butenhof:

Q56: Por que não preciso declarar variáveis ​​compartilhadas VOLATILE?

No entanto, estou preocupado com casos em que o compilador e a biblioteca de threads cumprem suas respectivas especificações. Um compilador C em conformidade pode alocar globalmente alguma variável compartilhada (não volátil) para um registro que é salvo e restaurado à medida que a CPU é passada de um thread para outro. Cada thread terá seu próprio valor privado para essa variável compartilhada, que não é o que queremos de uma variável compartilhada.

Em certo sentido, isso é verdade, se o compilador souber o suficiente sobre os respectivos escopos da variável e as funções pthread_cond_wait (ou pthread_mutex_lock). Na prática, a maioria dos compiladores não tentará manter cópias de dados globais de registro em uma chamada para uma função externa, porque é muito difícil saber se a rotina pode, de alguma forma, ter acesso ao endereço dos dados.

Portanto, sim, é verdade que um compilador que esteja em conformidade estrita (mas muito agressivamente) com o ANSI C pode não funcionar com vários threads sem volatilidade. Mas é melhor alguém consertar isso. Como qualquer sistema (ou seja, pragmaticamente, uma combinação de kernel, bibliotecas e compilador C) que não forneça as garantias de coerência de memória POSIX, não está em conformidade com o padrão POSIX. Período. O sistema NÃO PODE exigir que você use variáveis ​​voláteis em variáveis ​​compartilhadas para o comportamento correto, porque o POSIX exige apenas que as funções de sincronização do POSIX sejam necessárias.

Portanto, se o seu programa for interrompido porque você não usou volátil, isso é um BUG. Pode não ser um bug em C, ou um bug na biblioteca de threads ou um bug no kernel. Mas é um bug do sistema, e um ou mais desses componentes terão que trabalhar para corrigi-lo.

Você não deseja usar volátil, porque, em qualquer sistema em que isso faça alguma diferença, será muito mais caro que uma variável não volátil adequada. (O ANSI C requer "pontos de sequência" para variáveis ​​voláteis em cada expressão, enquanto o POSIX os exige apenas em operações de sincronização - um aplicativo encadeado intensivo em computação verá substancialmente mais atividade de memória usando volátil e, afinal, é a atividade de memória que realmente atrasa você.)

/ --- [Dave Butenhof] ----------------------- [butenhof@zko.dec.com] --- \
| Digital Equipment Corporation 110 Spit Brook Rd ZKO2-3 / Q18 |
| 603.881.2218, FAX 603.881.0120 Nashua NH 03062-2698 |
----------------- [Melhor Viver Através da Concorrência] ---------------- /

Butenhof aborda praticamente o mesmo terreno neste post da usenet :

O uso de "volátil" não é suficiente para garantir a visibilidade ou sincronização adequada da memória entre os threads. O uso de um mutex é suficiente e, exceto pelo recurso a várias alternativas de código de máquina não portáveis ​​((ou implicações mais sutis das regras de memória POSIX que são muito mais difíceis de aplicar em geral, como explicado em minha postagem anterior), um O mutex é NECESSÁRIO.

Portanto, como Bryan explicou, o uso de volátil não realiza nada além de impedir que o compilador faça otimizações úteis e desejáveis, não fornecendo ajuda alguma para tornar o código "thread safe". É claro que você pode declarar o que quiser como "volátil" - afinal, é um atributo de armazenamento ANSI C legal. Só não espere que ele resolva qualquer problema de sincronização de threads para você.

Tudo isso é igualmente aplicável ao C ++.


O link está quebrado; não parece mais apontar para o que você queria citar. Sem o texto, é um tipo de resposta sem sentido.
JWW

3

Isso é tudo o que "volátil" está fazendo: "Ei, compilador, essa variável pode mudar A QUALQUER MOMENTO (em qualquer marca de relógio), mesmo se NÃO houver INSTRUÇÕES LOCAIS atuando nele. NÃO coloque esse valor em cache em um registro".

É isso. Diz ao compilador que seu valor é, bem, volátil - esse valor pode ser alterado a qualquer momento pela lógica externa (outro encadeamento, outro processo, o Kernel etc.). Existe mais ou menos exclusivamente para suprimir otimizações do compilador que armazenam em cache silenciosamente um valor em um registro que é inerentemente inseguro para o cache EVER.

Você pode encontrar artigos como "Dr. Dobbs" que são voláteis como uma panacéia para programação multithread. Sua abordagem não é totalmente desprovida de mérito, mas tem a falha fundamental de tornar os usuários de um objeto responsáveis ​​por sua segurança de threads, que tende a ter os mesmos problemas que outras violações do encapsulamento.


3

De acordo com meu antigo padrão C, “o que constitui um acesso a um objeto que possui um tipo qualificado para uso volátil é definido pela implementação” . Assim, os criadores do compilador C poderiam ter optado por ter "volátil" significa "acesso seguro ao encadeamento em um ambiente de múltiplos processos" . Mas eles não fizeram.

Em vez disso, as operações necessárias para tornar um encadeamento de seção crítico seguro em um ambiente de memória compartilhada com vários processos e múltiplos núcleos foram adicionadas como novos recursos definidos pela implementação. E, livres do requisito de que "volátil" forneceria acesso atômico e pedido de acesso em um ambiente de múltiplos processos, os escritores do compilador priorizaram a redução de código em vez da semântica "volátil" histórica dependente da implementação.

Isso significa que coisas como semáforos "voláteis" em torno de seções críticas de código, que não funcionam em novo hardware com novos compiladores, podem ter funcionado com compiladores antigos em hardware antigo, e exemplos antigos às vezes não estão errados, apenas antigos.


Os exemplos antigos exigiam que o programa fosse processado por compiladores de qualidade adequados para programação de baixo nível. Infelizmente, os compiladores "modernos" consideram o fato de que o Padrão não exige que eles processem "voláteis" de uma maneira útil como uma indicação de que o código que exigiria que eles o fizessem está quebrado, em vez de reconhecer que o Padrão não esforço para proibir implementações que são conformes, mas de uma qualidade tão baixa a ponto de ser inútil, mas não de forma alguma tolerar baixa qualidade, mas conformes compiladores que se tornaram populares
supercat

Na maioria das plataformas, seria bastante fácil reconhecer o que volatileseria necessário para permitir a criação de um sistema operacional de uma maneira dependente do hardware, mas independente do compilador. Exigir que os programadores usem recursos dependentes da implementação em vez de fazer o volatiletrabalho conforme necessário prejudica o objetivo de ter um padrão.
Supercat
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.