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

Na linguagem de programação C e Pthreads como a biblioteca de threads; as variables ​​/ estruturas que são compartilhadas entre threads precisam ser declaradas como voláteis? Assumindo que eles podem ser protegidos por um bloqueio ou não (barreiras talvez).

O padrão POSIX pthread tem alguma opinião sobre isso, isso é dependente do compilador ou nenhum dos dois?

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

Eu acho que uma propriedade muito importante do volátil é que ele faz a variável ser gravada na memory quando modificada, e relida da memory toda vez que é acessada. As outras respostas aqui misturam volatilidade e synchronization, e fica claro, a partir de algumas outras respostas, que o volátil NÃO é um primitivo de sincronia (crédito onde o crédito é devido).

Mas a menos que você use volatile, o compilador está livre para armazenar em cache os dados compartilhados em um registro por qualquer período de tempo … se você quiser que seus dados sejam gravados para serem gravados previsivelmente na memory real e não apenas armazenados em cache em um registro pelo compilador a seu critério, você precisará marcá-lo como volátil. Alternativamente, se você acessar apenas os dados compartilhados depois de ter deixado uma function modificando-a, você pode estar bem. Mas sugiro não confiar na sorte cega para garantir que os valores sejam gravados de volta para a memory.

Especialmente em máquinas ricas em registradores (isto é, não em x86), as variables ​​podem viver por períodos bastante longos em registradores, e um bom compilador pode armazenar em cache mesmo partes de estruturas ou estruturas inteiras em registradores. Então você deve usar volátil, mas para desempenho, também copiar valores para variables ​​locais para computação e, em seguida, fazer um write-back explícito. Essencialmente, o uso eficiente de voláteis significa fazer um pouco de pensamento de armazenamento de carga em seu código em C.

Em qualquer caso, você tem que usar positivamente algum tipo de mecanismo de synchronization fornecido no nível do sistema operacional para criar um programa correto.

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

Contanto que você esteja usando bloqueios para controlar o access à variável, não é necessário que ela seja volátil. De fato, se você está colocando volatilidade em alguma variável, provavelmente você já está errado.

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

A resposta é absolutamente, inequivocamente, NÃO. Você não precisa usar ‘volátil’ além das primitivas de synchronization 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 synchronization adequadas são suficientes. Não é suficiente, porque apenas desativa algumas otimizações, não todas as que podem incomodá-lo. Por exemplo, não garante atomicidade nem visibilidade em outra CPU.

Mas a menos que você use volatile, o compilador está livre para armazenar em cache os dados compartilhados em um registro por qualquer período de tempo … se você quiser que seus dados sejam gravados para serem gravados previsivelmente na memory real e não apenas armazenados em cache em um registro pelo compilador a seu critério, você precisará marcá-lo como volátil. Alternativamente, se você acessar apenas os dados compartilhados depois de ter deixado uma function modificando-a, você pode estar bem. Mas sugiro não confiar na sorte cega para garantir que os valores sejam gravados de volta para a memory.

Certo, mas mesmo se você usar volátil, a CPU fica 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 incomodar você não é exatamente o mesmo que o conjunto de otimizações que ‘volátil’ desativa. Então, se você usa ‘volátil’, você está confiando na sorte.

Por outro lado, se você usar primitivos de synchronization com semântica multiencadeada definida, terá a garantia de que as coisas funcionarão. Como uma vantagem, você não pega o enorme impacto de 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 multi-threaded.

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

  • volatile pode ser usado para marcar variables ​​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, já 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. (Ainda não está claro qual fração de tais usos seria retardada, exigindo que todas as variables ​​sejam preservadas em um longjmp, mas isso é um assunto separado e não é considerado aqui.)
  • Volátil pode ser usado quando variables ​​podem ser “modificadas externamente”, mas a modificação de fato é triggersda de forma síncrona pelo próprio thread, por exemplo, porque a memory subjacente é mapeada em múltiplos locais.
  • Um sigatomic_t volátil pode ser usado para se comunicar com um manipulador de sinal no mesmo thread, de maneira restrita. Poderíamos considerar enfraquecer os requisitos para o caso sigatômico, mas isso parece bastante contra-intuitivo.

Se você é multi-threading por causa da velocidade, desacelerar o código definitivamente não é o que você quer. Para programação multi-threaded, existem dois problemas chave que o volátil é muitas vezes erroneamente pensado para resolver:

  • atomicidade
  • consistência de memory , ou seja, a ordem das operações de um encadeamento como visto por outro encadeamento.

Vamos lidar com (1) primeiro. O 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 a volatilidade não tem nada a ver com isso . Provavelmente seria atômico sem o volátil. A atomicidade está no capricho do compilador. Não há nada nos padrões C ou C ++ que diz que tem que ser atômico.

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

  volatile int Ready; int Message[100]; void foo( int i ) { Message[i/10] = 42; Ready = 1; } 

Ele está tentando fazer algo muito razoável em programação multi-threaded: escrever uma mensagem e, em seguida, enviá-lo para outro segmento. O outro thread aguardará até que o Ready se torne diferente de zero e, em seguida, leia Message. Tente compilar isso com “gcc -O2 -S” usando o gcc 4.0 ou icc. Ambos farão o armazenamento para o Ready primeiro, então ele pode ser sobreposto com o 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 memory voláteis. Isso é simplesmente bobo. Como as citações anteriores dizem, isso apenas deixará seu código mais lento. Pior ainda, pode não resolver o problema. Mesmo se o compilador não reordenar as referências, o hardware poderá. Neste exemplo, o hardware x86 não irá reordená-lo. Nem um processador Itanium (TM), porque os compiladores do Itanium inserem cercas de memory para armazenamentos voláteis. Essa é uma inteligente extensão Itanium. Mas chips como o Power (TM) serão reordenados. O que você realmente precisa para encomendar são cercas de memory , também chamadas de barreiras de memory . Uma cerca de memory impede a reordenação de operações de memory através da cerca ou, em alguns casos, impede a reordenação em uma direção.Volátil não tem nada a ver com cercas de memory.

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

  • Tópicos POSIX
  • Tópicos do Windows (TM)
  • OpenMP
  • TBB

Baseado no artigo de Arch Robison (Intel)

Na minha experiência, não; você só precisa se mutexicar corretamente quando gravar nesses valores, ou estruturar seu programa de forma que os threads parem antes que eles precisem acessar os dados que dependem das ações de outro thread. Meu projeto, x264, usa esse método; Os threads compartilham uma enorme quantidade de dados, mas a grande maioria deles não precisa de mutexes, porque ele é somente leitura ou um thread aguardará os dados se tornarem disponíveis e finalizados antes de precisar acessá-los.

Agora, se você tem muitos segmentos que são muito intercalados em suas operações (eles dependem da produção 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 ele pode ser feito de forma mais limpa com mais separação entre threads.

NÃO.

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

O principal uso do volatile é o access à E / S mapeada na memory. Nesse caso, o dispositivo subjacente pode alterar o valor de um local de memory independentemente da CPU. Se você não usar volatile sob essa condição, a CPU poderá usar um valor de memory armazenado anteriormente em cache, em vez de ler o valor recém-atualizado.

Volatile só seria útil se você não precisasse de nenhum atraso entre quando um thread escreve algo e outro thread o lê. Sem algum tipo de bloqueio, porém, você não tem idéia de quando o outro segmento escreveu 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 synchronization explícito. Se você não usar um mutex ou bloqueio de algum tipo, você deve declarar a variável volátil. Se você usa 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 semi-alterada sem um mutex.

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

O que isto significa é que você deve marcar variables ​​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 de um segundo thread aparecer e tente ler o valor .

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

Isso é especialmente importante se você NÃO estiver usando os caros objects de synchronization de encadeamentos fornecidos pelo seu sistema – você pode, por exemplo, ter uma estrutura de dados em que possa mantê-la válida com uma série de mudanças atômicas. Muitas pilhas que não alocam memory são exemplos dessas estruturas de dados, porque você pode adicionar um valor à pilha e, em seguida, mover o ponteiro final ou remover um valor da pilha depois de mover o ponteiro final. Ao implementar essa estrutura, o 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 uma máquina abstrata de encadeamento único . E o compilador está em seu próprio direito de transformar o programa, desde que os ‘comportamentos observáveis’ do programa na máquina abstrata permaneçam inalterados. Ele pode mesclar accesss de memory adjacentes ou sobrepostos, refazer um access à memory várias vezes (por exemplo, registrar o derramamento) ou simplesmente descartar um access à memory, se achar que os comportamentos do programa, quando executados em um único thread , não mudam. Portanto, como você pode suspeitar, os comportamentos mudam, se o programa, na verdade, deveria estar sendo executado de uma maneira multi-threaded.

Como Paul Mckenney apontou em um documento famoso sobre o kernel do Linux :

Não é necessário assumir que o compilador fará o que você deseja com referências de memory que não são protegidas por READ_ONCE () e WRITE_ONCE (). Sem eles, o compilador tem o direito de fazer todos os tipos de transformações “criativas”, que são abordadas na seção BARREIRA DO COMPILADOR.

READ_ONCE () e WRITE_ONCE () são definidos como conversões voláteis em variables ​​referenciadas. Portanto:

 int y; int x = READ_ONCE(y); 

é equivalente a:

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

Portanto, a menos que você faça um access “volátil”, não terá certeza de que o access acontecerá exatamente uma vez , independentemente do mecanismo de synchronization usado. Chamar uma function externa (pthread_mutex_lock, por exemplo) pode forçar o compilador a acessar a memory para variables ​​globais. Mas isso acontece somente quando o compilador não consegue descobrir se a function externa altera essas variables ​​globais ou não. Compiladores modernos que empregam análise sofisticada entre procedimentos e otimização de link-time tornam esse truque simplesmente inútil.

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


Como Paul McKenney também apontou:

Eu vi o brilho em seus olhos quando eles discutem técnicas de otimização que você não gostaria que seus filhos soubessem!


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

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

Volátil significa que a variável é atualizada fora do escopo do código e, portanto, o compilador não pode assumir que ela saiba o valor atual dela. Mesmo as barreiras de memory são inúteis, já que o compilador, que está alheio às barreiras de memory (certo?), Ainda pode usar um valor em cache.

Algumas pessoas obviamente estão assumindo que o compilador trata as chamadas de synchronization como barreiras de memory. “Casey” está assumindo que há exatamente uma CPU.

Se as primitivas de synchronization forem funções externas e os símbolos em questão estiverem visíveis fora da unidade de compilation (nomes globais, ponteiro exportado, function exportada que pode modificá-los), o compilador os tratará – ou qualquer outra chamada de function externa – como um cerca de memory em relação a todos os objects visíveis externamente.

Caso contrário, você está sozinho. E volátil pode ser a melhor ferramenta disponível para fazer o compilador produzir código rápido e correto. Geralmente não será portátil, quando você precisa de voláteis e o que 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ânticas multithread garantidas que não usam volatile . Estes incluem operações atômicas, mutexes e assim por diante.

Em segundo lugar, volatile não é suficiente. O padrão C não fornece nenhuma garantia sobre o comportamento multithread para variables ​​declaradas volatile .

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

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

As variables ​​que são compartilhadas entre os segmentos devem ser declaradas como “voláteis”. Isso diz ao compilador que quando um thread grava em tais variables, a gravação deve estar na memory (em oposição a um registrador).