Como demonstrado nesta resposta que postei recentemente, pareço estar confuso sobre a utilidade (ou a falta dela) volatile
em 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 foo
e 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, foo
deve 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?
Respostas:
O problema
volatile
em 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 confiarvolatile
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:
volatile
variá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 ?volatile
garante o primeiro ponto. Também garante que nenhuma reordenação ocorra entre diferentes leituras / gravações voláteis . Todosvolatile
os acessos à memória ocorrerão na ordem em que foram especificados. É tudo o que precisamos para o quevolatile
se destina: manipular registros de E / S ou hardware mapeado na memória, mas isso não nos ajuda no código multithread, onde ovolatile
objeto geralmente é usado apenas para sincronizar o acesso a dados não voláteis. Esses acessos ainda podem ser reordenados em relaçãovolatile
à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
volatile
desnecessário. Podemos apenas removervolatile
completamente o qualificador.Desde o C ++ 11, as variáveis atômicas (
std::atomic<T>
) nos dão todas as garantias relevantes.fonte
volatile
não são fortes o suficiente para serem úteis.volatile
ser 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.volatile
é sempre inútil para programação multithread. (Exceto no Visual Studio, onde volátil é a extensão da barreira da memória.)Você também pode considerar isso na documentação do kernel do Linux .
fonte
volatile
sentido. Em todos os casos, o comportamento "chamar uma função cujo corpo não pode ser visto" estará correto.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.
fonte
mutex_lock
(e todas as outras funções da biblioteca) podem alterar o estado da variável flag.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 ++volatile
nã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.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 outrovolatile
.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 terIsso 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
volatile
dar garantias extras sobre operações comuns .Agora você tem algo parecido com isto:
my_shared_flag
não precisa ser volátil, apesar de inatingível, porque&
operador).pthread_mutex_lock
é uma função de biblioteca.pthread_mutex_lock
alguma forma adquire essa referência.pthread_mutex_lock
modifica o sinalizador compartilhado !volatile
, embora significativo nesse contexto, é estranho.fonte
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):
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):
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.
ainda pode ser otimizado para
se o otimizador for inteligente o suficiente (não altera a semântica do código).
fonte
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
fonte
A FAQ do comp.programming.threads tem uma explicação clássica de Dave Butenhof:
Butenhof aborda praticamente o mesmo terreno neste post da usenet :
Tudo isso é igualmente aplicável ao C ++.
fonte
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.
fonte
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.
fonte
volatile
seria 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 ovolatile
trabalho conforme necessário prejudica o objetivo de ter um padrão.