Quando usar o volátil com o multi threading?

130

Se houver dois threads acessando uma variável global, muitos tutoriais dizem tornar a variável volátil para impedir que o compilador armazene em cache a variável em um registro e, portanto, não seja atualizado corretamente. No entanto, dois threads que acessam uma variável compartilhada são algo que exige proteção através de um mutex, não é? Mas nesse caso, entre o encadeamento e liberação do mutex, o código está em uma seção crítica, onde apenas um encadeamento pode acessar a variável. Nesse caso, a variável não precisa ser volátil?

Portanto, qual é o uso / objetivo do volátil em um programa multiencadeado?

David Preston
fonte
3
Em alguns casos, você não deseja / precisa de proteção pelo mutex.
Stefan Mai
4
Às vezes, é bom ter uma condição de corrida, às vezes não. Como você está usando essa variável?
David Heffernan
3
@ David: Um exemplo de quando é "bom" ter uma corrida, por favor?
John Dibling
6
@ John Aqui vai. Imagine que você tem um segmento de trabalho que está processando várias tarefas. O segmento de trabalho incrementa um contador sempre que termina uma tarefa. O encadeamento mestre lê periodicamente esse contador e atualiza o usuário com notícias do progresso. Desde que o contador esteja alinhado corretamente para evitar rasgos, não há necessidade de sincronizar o acesso. Embora exista uma corrida, é benigna.
David Heffernan
5
@John O hardware no qual esse código é executado garante que variáveis ​​alinhadas não sofram rasgos. Se o trabalhador estiver atualizando n para n + 1 enquanto o leitor lê, ele não se importa se recebe n ou n + 1. Nenhuma decisão importante será tomada, pois ela é usada apenas para relatórios de progresso.
David Heffernan

Respostas:

167

Resposta curta e rápida : volatileé (quase) inútil para programação de aplicativos multithread, independente de plataforma. Não fornece nenhuma sincronização, não cria cercas de memória, nem garante a ordem de execução das operações. Não torna as operações atômicas. Não torna seu código magicamente seguro para threads. volatilepode ser a instalação mais incompreendida em todo o C ++. Veja isto , isto e isto para obter mais informações sobrevolatile

Por outro lado, volatiletem algum uso que pode não ser tão óbvio. Ele pode ser usado da mesma maneira que seria usado constpara ajudar o compilador a mostrar onde você pode estar cometendo um erro ao acessar algum recurso compartilhado de maneira não protegida. Esse uso é discutido por Alexandrescu neste artigo . No entanto, isso é basicamente usando o sistema do tipo C ++ de uma maneira que muitas vezes é vista como um artifício e pode evocar o comportamento indefinido.

volatilefoi projetado especificamente para ser usado na interface com hardware mapeado na memória, manipuladores de sinal e instruções de código de máquina setjmp. Isso torna volatilediretamente aplicável à programação no nível de sistemas, em vez da programação normal no nível de aplicativos.

O padrão C ++ de 2003 não diz que volatilese aplica qualquer tipo de semântica de aquisição ou liberação em variáveis. De fato, o Padrão é completamente silencioso em todos os assuntos de multithreading. No entanto, plataformas específicas aplicam a semântica Adquirir e Liberar em volatilevariáveis.

[Atualização para C ++ 11]

O C ++ 11 Padrão agora faz Reconhecer multithreading diretamente no modelo de memória ea lanuage, e fornece serviços de biblioteca para lidar com isso de uma maneira de plataforma independente. No entanto, a semântica de volatileainda não mudou. volatileainda não é um mecanismo de sincronização. Bjarne Stroustrup diz o mesmo no TCPPPL4E:

Não use, volatileexceto no código de baixo nível que lida diretamente com o hardware.

Não assuma que volatiletem um significado especial no modelo de memória. Isso não. Não é - como em alguns idiomas posteriores - um mecanismo de sincronização. Para obter sincronização, use atomic, a mutexou a condition_variable.

[/ Fim da atualização]

O acima exposto aplica-se à própria linguagem C ++, conforme definido pelo Padrão de 2003 (e agora o Padrão de 2011). Algumas plataformas específicas, no entanto, adicionam funcionalidade ou restrições adicionais ao que volatilefaz. Por exemplo, em MSVC 2010 (pelo menos) Adquirir e liberar semântica não se aplicam a determinadas operações volatilevariáveis. Do MSDN :

Ao otimizar, o compilador deve manter a ordem entre referências a objetos voláteis, bem como referências a outros objetos globais. Em particular,

Uma gravação em um objeto volátil (gravação volátil) possui semântica de liberação; uma referência a um objeto global ou estático que ocorre antes de uma gravação em um objeto volátil na sequência de instruções ocorrerá antes dessa gravação volátil no binário compilado.

Uma leitura de um objeto volátil (leitura volátil) possui Semântica; uma referência a um objeto global ou estático que ocorre após uma leitura da memória volátil na sequência de instruções ocorrerá após essa leitura volátil no binário compilado.

No entanto, você pode observar o fato de que, se você seguir o link acima, há algum debate nos comentários sobre se a aquisição / liberação de semântica realmente se aplica neste caso.

John Dibling
fonte
19
Parte de mim quer rebater isso por causa do tom condescendente da resposta e do primeiro comentário. "volátil é inútil" é semelhante a "alocação manual de memória é inútil". Se você pode escrever um programa multithread sem volatileele, é porque ficou sobre os ombros de pessoas que costumavam volatileimplementar bibliotecas de threading.
Ben Jackson
19
@ Ben só porque algo desafia suas crenças não significa que seja condescendente
David Heffernan
38
@ Ben: não, leia o que volatilerealmente faz em C ++. O que @John disse está correto , fim da história. Não tem nada a ver com código de aplicativo x código de biblioteca ou "programadores oniscientes" comuns "versus" divinos "". volatileé desnecessário e inútil para sincronização entre threads. As bibliotecas de encadeamento não podem ser implementadas em termos de volatile; de qualquer maneira, ele precisa confiar nos detalhes específicos da plataforma e, quando você depende deles, não precisa mais volatile.
jalf
6
@jalf: "volátil é desnecessário e inútil para sincronização entre threads" (que é o que você disse) não é a mesma coisa que "volátil é inútil para programação multithread" (que é o que John disse na resposta). Você está 100% correto, mas não concordo com John (parcialmente) - volátil ainda pode ser usado para a programação multithreaded (para um conjunto muito limitado de tarefas)
4
@ GMan: Tudo o que é útil só é útil sob um determinado conjunto de requisitos ou condições. O volátil é útil para programação multithread sob um conjunto estrito de condições (e, em alguns casos, pode até ser melhor (para alguma definição de melhor) do que alternativas). Você diz "ignorando isso e aquilo" ... mas o caso quando volátil é útil para multithreading não ignora nada. Você inventou algo que eu nunca reivindiquei. Sim, a utilidade do volátil é limitada, mas existe - mas todos podemos concordar que NÃO é útil para sincronização.
31

(Nota do editor: em C ++ 11 volatilenão é a ferramenta certa para este trabalho e ainda possui UB de corrida de dados. Use std::atomic<bool>com std::memory_order_relaxedcargas / armazenamentos para fazer isso sem UB. Em implementações reais, ele será compilado da mesma forma volatileque adicionei. uma resposta com mais detalhes e também abordando os conceitos errôneos nos comentários de que a memória com ordem fraca pode ser um problema para este caso de uso: todas as CPUs do mundo real têm memória compartilhada coerente, portanto volatile, trabalhará com isso em implementações reais em C ++. faça isso.

Alguma discussão nos comentários parece estar falando sobre outros casos de uso onde você iria precisar de algo mais forte do que atomics relaxado. Essa resposta já aponta que volatilevocê não solicita.)


O volátil é ocasionalmente útil pelo seguinte motivo: este código:

/* global */ bool flag = false;

while (!flag) {}

é otimizado pelo gcc para:

if (!flag) { while (true) {} }

O que obviamente está incorreto se o sinalizador for gravado no outro thread. Observe que, sem essa otimização, o mecanismo de sincronização provavelmente funciona (dependendo do outro código, algumas barreiras de memória podem ser necessárias) - não há necessidade de um mutex em 1 produtor - 1 cenário de consumidor.

Caso contrário, a palavra-chave volátil é muito estranha para ser utilizável - ela não fornece nenhuma garantia de pedidos de memória por acessos voláteis e não voláteis e não fornece operações atômicas - ou seja, você não recebe ajuda do compilador com palavras-chave voláteis, exceto o cache de registro desabilitado .

zeuxcg
fonte
4
Se bem me lembro, C ++ 0x atômico, é feito para fazer corretamente o que muitas pessoas acreditam (incorretamente) é feito por volátil.
David Heffernan
13
volatilenão impede que os acessos à memória sejam reordenados. volatileacessos não será reordenada com respeito a cada um dos outros, mas eles fornecem nenhuma garantia sobre a reordenação em relação ao não- volatileobjetos, e assim, eles são basicamente inútil como bandeiras também.
jalf
13
@ Ben: Eu acho que você tem de cabeça para baixo. A multidão "volátil é inútil" confia no simples fato de que o volátil não protege contra a reordenação , o que significa que é totalmente inútil para a sincronização. Outras abordagens podem ser igualmente inúteis (como você mencionou, a otimização do código em tempo de link pode permitir que o compilador espie o código que você supôs que o compilador trataria como uma caixa preta), mas isso não corrige as deficiências volatile.
jalf
15
@jalf: Veja o artigo de Arch Robinson (link para outro local nesta página), décimo comentário (por "Spud"). Basicamente, a reordenação não altera a lógica do código. O código publicado usa o sinalizador para cancelar uma tarefa (em vez de sinalizar que a tarefa está concluída), portanto, não importa se a tarefa é cancelada antes ou depois do código (por exemplo while (work_left) { do_piece_of_work(); if (cancel) break;}, se o cancelamento é reordenado dentro do loop, a lógica ainda é válido Eu tinha um pedaço de código que trabalhou semelhante: se a thread principal quer terminar, ele define o sinalizador para outros tópicos, mas isso não acontece ....
15
... importa se os outros threads fazem algumas iterações extras de seus loops de trabalho antes de terminarem, desde que isso aconteça razoavelmente logo após o sinalizador ser definido. Obviamente, esse é o ÚNICO uso que eu consigo pensar e seu nicho (e pode não funcionar em plataformas nas quais a gravação em uma variável volátil não torna a alteração visível para outros threads, embora em pelo menos x86 e x86-64 isso trabalho). Eu certamente não aconselharia ninguém a fazer isso sem uma boa razão, apenas estou dizendo que uma declaração geral como "volátil NUNCA é útil em código multithread" não está 100% correta.
15

No C ++ 11, normalmente nunca use volatilepara segmentação, apenas para o MMIO

Mas TL: DR, ele "funciona" como atômico mo_relaxedno hardware com caches coerentes (ou seja, tudo); é suficiente parar os compiladores que mantêm vars nos registros. atomicnão precisa de barreiras de memória para criar atomicidade ou visibilidade entre encadeamentos, apenas para fazer com que o encadeamento atual aguarde antes / depois de uma operação para criar pedidos entre os acessos desse encadeamento a diferentes variáveis. mo_relaxednunca precisa de barreiras, basta carregar, armazenar ou RMW.

Para atômicos autônomos com volatile(e inline-asm para barreiras) nos velhos tempos anteriores ao C ++ 11 std::atomic, volatileera a única maneira boa de fazer algumas coisas funcionarem . Mas isso dependia de muitas suposições sobre como as implementações funcionavam e nunca foi garantido por nenhum padrão.

Por exemplo, o kernel do Linux ainda usa seus próprios átomos enrolados manualmente volatile, mas suporta apenas algumas implementações específicas de C (GNU C, clang e talvez ICC). Em parte, isso se deve às extensões do GNU C e à sintaxe e semântica inline do asm, mas também porque depende de algumas suposições sobre como os compiladores funcionam.

É quase sempre a escolha errada para novos projetos; você pode usar std::atomic(with std::memory_order_relaxed) para fazer com que um compilador emita o mesmo código de máquina eficiente que você poderia usar volatile. std::atomiccom mo_relaxedobsoletos volatilepara fins de segmentação. (exceto talvez para solucionar erros de otimização perdida atomic<double>em alguns compiladores .)

A implementação interna dos std::atomiccompiladores convencionais (como gcc e clang) não é usada apenas volatileinternamente; compiladores expõem diretamente funções atômicas de carga, armazenamento e RMW. (por exemplo, embutidos no GNU C__atomic que operam em objetos "simples".)


Volátil é utilizável na prática (mas não faça isso)

Dito isto, volatileé útil na prática para coisas como um exit_nowsinalizador em todas as implementações C ++ existentes em CPUs reais, por causa de como as CPUs funcionam (caches coerentes) e por suposições compartilhadas sobre como volatiledeve funcionar. Mas não muito mais, e não é recomendado. O objetivo desta resposta é explicar como as implementações existentes de CPUs e C ++ realmente funcionam. Se você não se importa com isso, tudo o que precisa saber é que, std::atomiccom os obsoletos mo_relaxed volatilepara segmentação.

(O padrão ISO C ++ é bastante vago, apenas dizendo que os volatileacessos devem ser avaliados estritamente de acordo com as regras da máquina abstrata C ++, não otimizada. Dado que implementações reais usam o espaço de endereço da memória da máquina para modelar o espaço de endereço C ++, isso significa que volatileleituras e atribuições precisam ser compiladas para carregar / armazenar instruções para acessar a representação de objeto na memória.)


Como outra resposta aponta, um exit_nowsinalizador é um caso simples de comunicação entre threads que não precisa de sincronização : não está publicando se o conteúdo da matriz está pronto ou algo parecido. Apenas uma loja que é notada imediatamente por uma carga ausente não otimizada em outro encadeamento.

    // global
    bool exit_now = false;

    // in one thread
    while (!exit_now) { do_stuff; }

    // in another thread, or signal handler in this thread
    exit_now = true;

Sem volátil ou atômica, a regra como se e a suposição de que nenhum UB de corrida de dados permite que um compilador o otimize para asm que verifica apenas o sinalizador uma vez , antes de inserir (ou não) um loop infinito. É exatamente o que acontece na vida real para compiladores reais. (E geralmente otimiza muito do_stuffporque o loop nunca sai, portanto, qualquer código posterior que possa ter usado o resultado não é alcançável se inserirmos o loop).

 // Optimizing compilers transform the loop into asm like this
    if (!exit_now) {        // check once before entering loop
        while(1) do_stuff;  // infinite loop
    }

O programa multithreading travou no modo otimizado, mas é executado normalmente em -O0 é um exemplo (com descrição da saída asm do GCC) de como exatamente isso acontece com o GCC no x86-64. Também programação MCU - a otimização do C ++ O2 é interrompida enquanto o loop na eletrônica.SE mostra outro exemplo.

Normalmente, queremos otimizações agressivas que o CSE e içam cargas fora dos loops, inclusive para variáveis ​​globais.

Antes do C ++ 11, volatile bool exit_nowhavia uma maneira de fazer isso funcionar conforme o esperado (em implementações normais do C ++). Mas no C ++ 11, o UB de corrida de dados ainda se aplica, volatileportanto, não é realmente garantido pelo padrão ISO que funcione em qualquer lugar, mesmo assumindo caches coerentes de HW.

Observe que, para tipos mais amplos, volatilenão oferece garantia de falta de rasgo. Eu ignorei essa distinção aqui boolporque é um problema nas implementações normais. Mas isso também é parte do motivo pelo qual volatileainda está sujeito à UB de corrida de dados, em vez de ser equivalente a um atômico relaxado.

Observe que "conforme pretendido" não significa que o thread exit_nowaguarda o outro thread sair. Ou mesmo que aguarde até que o exit_now=truearmazenamento volátil seja visível globalmente antes de continuar com as operações posteriores neste encadeamento. ( atomic<bool>com o padrão mo_seq_cst, esperaria antes que qualquer seq_cst mais tarde fosse carregado. Em muitos ISAs, você obteria uma barreira completa após a loja).

O C ++ 11 fornece uma maneira não UB que compila o mesmo

Um sinalizador "continue executando" ou "sair agora" deve ser usado std::atomic<bool> flagcommo_relaxed

Usando

  • flag.store(true, std::memory_order_relaxed)
  • while( !flag.load(std::memory_order_relaxed) ) { ... }

fornecerá exatamente o mesmo ASM (sem instruções de barreira caras) que você obteria volatile flag.

Além de não rasgar, atomictambém oferece a capacidade de armazenar em um thread e carregar em outro sem UB, para que o compilador não possa elevar a carga de um loop. (A suposição de que não há UB de corrida de dados é o que permite as otimizações agressivas que desejamos para objetos não-atômicos e não-voláteis.) Esse recurso atomic<T>é praticamente o mesmo que o volatilefaz para cargas e armazenamentos puros.

atomic<T>também faça +=e assim por diante em operações atômicas de RMW (significativamente mais caras que uma carga atômica em um temporário, opere e, em seguida, em um armazenamento atômico separado. Se você não quiser um RMW atômico, escreva seu código com um temporário local).

Com o seq_cstpedido padrão que você obteria while(!flag), ele também adiciona garantias de pedido. acessos não atômicos e para outros acessos atômicos.

(Em teoria, o padrão ISO C ++ não descarta a otimização de átomos em tempo de compilação. Mas, na prática, os compiladores não, porque não há como controlar quando isso não seria bom. Existem alguns casos em volatile atomic<T>que nem ser suficiente controle sobre otimização de atomics se compiladores fez otimizar, então por enquanto compiladores não. Veja por que não fazer compiladores fundir std :: redundante gravações atômicas? Note que wg21 / p0062 recomenda contra o uso volatile atomicde código atual para se proteger contra a otimização de atômica.)


volatile realmente funciona para isso em CPUs reais (mas ainda não o usa)

mesmo com modelos de memória com ordem fraca (não x86) . Mas na verdade não use, use atomic<T>em mo_relaxedvez disso !! O objetivo desta seção é abordar conceitos errôneos sobre como as CPUs reais funcionam, não para justificar volatile. Se você estiver escrevendo código sem bloqueio, provavelmente se preocupa com o desempenho. Compreender caches e os custos da comunicação entre threads geralmente é importante para um bom desempenho.

CPUs reais têm caches coerentes / memória compartilhada: depois que uma loja de um núcleo se torna globalmente visível, nenhum outro núcleo pode carregar um valor obsoleto. (Veja também Myths Programmers Believe on CPU Caches, que fala um pouco sobre os voláteis Java, equivalentes ao C ++ atomic<T>com ordem de memória seq_cst.)

Quando digo carregar , quero dizer uma instrução asm que acessa a memória. É isso que um volatileacesso garante e não é a mesma coisa que a conversão de valor em valor de uma variável C ++ não atômica / não volátil. (por exemplo, local_tmp = flagou while(!flag)).

A única coisa que você precisa derrotar são as otimizações em tempo de compilação que não são recarregadas após a primeira verificação. Qualquer carga + verificação em cada iteração é suficiente, sem qualquer pedido. Sem sincronização entre esse encadeamento e o encadeamento principal, não faz sentido falar sobre quando exatamente ocorreu a loja ou a ordem do carregamento da carga. outras operações no loop. Somente quando é visível para esta discussão é o que importa. Quando você vê o sinalizador exit_now definido, você sai. A latência entre núcleos em um xeon x86 típico pode ser algo como 40ns entre núcleos físicos separados .


Em teoria: threads C ++ em hardware sem caches coerentes

Não vejo como isso possa ser remotamente eficiente, com apenas ISO C ++ puro, sem exigir que o programador faça alterações explícitas no código-fonte.

Em teoria, você poderia ter uma implementação C ++ em uma máquina que não fosse assim, exigindo liberações explícitas geradas pelo compilador para tornar as coisas visíveis para outros threads em outros núcleos . (Ou para leituras para não usar uma cópia talvez obsoleta). O padrão C ++ não torna isso impossível, mas o modelo de memória do C ++ é projetado para ser eficiente em máquinas coerentes de memória compartilhada. Por exemplo, o padrão C ++ fala sobre "coerência de leitura e leitura", "coerência de gravação e leitura", etc. Uma observação no padrão aponta até a conexão com o hardware:

http://eel.is/c++draft/intro.races#19

[Nota: Os quatro requisitos de coerência precedentes proíbem efetivamente a reordenação do compilador de operações atômicas em um único objeto, mesmo se as duas operações forem cargas relaxadas. Isso efetivamente torna a garantia de coerência do cache fornecida pela maioria dos hardwares disponíveis para operações atômicas em C ++. - nota final]

Não existe um mecanismo para uma releaseloja apenas liberar a si própria e alguns intervalos de endereços selecionados: ela precisaria sincronizar tudo, porque não saberia o que os outros encadeamentos poderiam querer ler se sua carga de aquisição visse essa loja de lançamento (formando um Seqüência de liberação que estabelece uma relação de antes do acontecimento entre os segmentos, garantindo que as operações não atômicas anteriores realizadas pelo segmento de gravação sejam seguras de ler. A menos que ele tenha gravado mais após o repositório de lançamentos ...) ser realmente esperto ao provar que apenas algumas linhas de cache precisavam ser liberadas.

Relacionado: minha resposta em Mov + mfence é seguro no NUMA? entra em detalhes sobre a inexistência de sistemas x86 sem memória compartilhada coerente. Também relacionado: Carrega e armazena a reordenação no ARM para saber mais sobre cargas / lojas no mesmo local.

Não são eu acho clusters com memória não-coerente compartilhada, mas eles não são máquinas de sistema de imagem única. Cada domínio de coerência executa um kernel separado, portanto você não pode executar threads de um único programa C ++ nele. Em vez disso, você executa instâncias separadas do programa (cada uma com seu próprio espaço de endereço: ponteiros em uma instância não são válidos na outra).

Para que eles se comuniquem entre si por liberações explícitas, você normalmente usaria o MPI ou outra API de passagem de mensagens para fazer o programa especificar quais intervalos de endereços precisam ser liberados.


O hardware real não é executado std::threadatravés dos limites de coerência do cache:

Existem alguns chips ARM assimétricos, com espaço de endereço físico compartilhado, mas não domínios de cache compartilhável interno. Portanto, não coerente. (por exemplo, fio comentário um núcleo A8 e um Cortex-M3 como TI Sitara AM335x).

Mas kernels diferentes rodariam nesses núcleos, não uma única imagem do sistema que pudesse executar threads nos dois núcleos. Não conheço nenhuma implementação de C ++ que execute std::threadthreads nos núcleos da CPU sem caches coerentes.

Para o ARM especificamente, o GCC e o clang geram código, assumindo que todos os threads sejam executados no mesmo domínio compartilhável interno. De fato, o manual do ARMv7 ISA diz

Essa arquitetura (ARMv7) é escrita com a expectativa de que todos os processadores que usam o mesmo sistema operacional ou hipervisor estejam no mesmo domínio de compartilhabilidade interna

Portanto, a memória compartilhada não coerente entre domínios separados é apenas uma coisa para o uso explícito específico do sistema de regiões de memória compartilhada para comunicação entre diferentes processos em diferentes kernels.

Consulte também esta discussão do CoreCLR sobre a geração de código usando dmb ishbarreiras de dmb symemória ( barreira compartilhável interna) vs. (sistema) nesse compilador.

Faço a afirmação de que nenhuma implementação C ++ para outro ISA é executado std::threadem núcleos com caches não coerentes. Não tenho provas de que não exista essa implementação, mas parece altamente improvável. A menos que você esteja direcionando uma parte exótica específica de HW que funcione dessa maneira, seu pensamento sobre desempenho deve assumir coerência de cache semelhante a MESI entre todos os threads. (De preferência, use atomic<T>de maneira a garantir a correção!)


Caches coerentes simplificam

Porém, em um sistema com vários núcleos com caches coerentes, implementar um armazenamento de versão significa apenas encomendar commit no cache para os armazenamentos desse encadeamento, sem fazer nenhuma liberação explícita. ( https://preshing.com/20120913/acquire-and-release-semantics/ e https://preshing.com/20120710/memory-barriers-are-like-source-control-operations/ ). (E uma carga de aquisição significa solicitar acesso ao cache no outro núcleo).

Uma instrução de barreira de memória apenas bloqueia as cargas e / ou armazena o encadeamento atual até que o buffer de armazenamento seja drenado; isso sempre acontece o mais rápido possível por conta própria. ( Uma barreira de memória garante que a coerência do cache foi concluída? Aborda esse equívoco). Portanto, se você não precisar fazer pedidos, basta avisar a visibilidade em outros threads mo_relaxed. (E assim é volatile, mas não faça isso.)

Consulte também mapeamentos C / C ++ 11 para processadores

Curiosidade: no x86, toda loja asm é uma loja de lançamento, porque o modelo de memória x86 é basicamente seq-cst mais um buffer de loja (com encaminhamento de loja).


Buffer semi-relacionado re: store, visibilidade global e coerência: o C ++ 11 garante muito pouco. A maioria dos ISAs reais (exceto o PowerPC) garante que todos os threads possam concordar com a ordem de aparência de dois armazenamentos por outros dois threads. (Na terminologia formal do modelo de memória da arquitetura do computador, eles são "atômica com várias cópias").

Outro equívoco é que são necessárias instruções de cerca de memória ASM para liberar o buffer de loja para outros núcleos para ver nossas lojas em tudo . Na verdade, o buffer de armazenamento está sempre tentando se drenar (comprometer-se com o cache L1d) o mais rápido possível, caso contrário, seria preenchido e paralisado a execução. O que uma barreira / barreira completa faz é interromper o encadeamento atual até que o buffer da loja seja drenado , para que nossas cargas posteriores apareçam na ordem global após as lojas anteriores.

(x86 está fortemente ordenou meios modelo de memória asm que volatileem x86 pode acabar dando-lhe mais perto mo_acq_rel, só que em tempo de compilação reordenação com variáveis não-atômicas ainda pode acontecer. Mas a maioria dos não-x86 têm modelos de memória fraca ordenada por isso volatilee relaxedsão quase tão fraco como mo_relaxedpermite.)

Peter Cordes
fonte
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
Samuel Liew
2
Ótima redação. Isso é exatamente o que eu estava procurando (fornecendo todos os fatos) em vez de uma declaração geral que apenas diz "use atômico em vez de volátil para uma única bandeira booleana compartilhada global".
Bernie
2
@bernie: escrevi isso depois de ficar frustrado com repetidas alegações de que o não uso atomicpoderia levar a threads diferentes com valores diferentes para a mesma variável no cache . / facepalm. No cache, não, no CPU registra sim (com variáveis ​​não atômicas); As CPUs usam cache coerente. Eu gostaria que outras perguntas sobre o SO não estivessem cheias de explicações para atomicesses conceitos errôneos sobre como as CPUs funcionam. (Porque isso é uma coisa útil para entender por motivos de desempenho, e também ajuda a explicar por que as regras atômicas do ISO C ++ são escritos como eles são.)
Peter Cordes
-1
#include <iostream>
#include <thread>
#include <unistd.h>
using namespace std;

bool checkValue = false;

int main()
{
    std::thread writer([&](){
            sleep(2);
            checkValue = true;
            std::cout << "Value of checkValue set to " << checkValue << std::endl;
        });

    std::thread reader([&](){
            while(!checkValue);
        });

    writer.join();
    reader.join();
}

Certa vez, um entrevistador que também acreditava que a volatilidade é inútil argumentou comigo que a otimização não causaria problemas e estava se referindo a diferentes núcleos com linhas de cache separadas e tudo isso (realmente não entendia exatamente o que ele estava se referindo). Mas esse pedaço de código, quando compilado com -O3 em g ++ (g ++ -O3 thread.cpp -lpthread), mostra um comportamento indefinido. Basicamente, se o valor for definido antes da verificação do tempo, ele funcionará bem e, caso contrário, ele entrará em um loop sem se preocupar em buscar o valor (que na verdade foi alterado pelo outro encadeamento). Basicamente, acredito que o valor de checkValue só é buscado uma vez no registro e nunca é verificado novamente sob o mais alto nível de otimização. Se definido como true antes da busca, funciona bem e, se não, entra em loop. Por favor, corrija-me se estiver errado.

Anu Siril
fonte
4
O que isso tem a ver volatile? Sim, esse código é UB - mas também é UB volatile.
David Schwartz
-2

Você precisa de volátil e possivelmente bloqueio.

volátil informa ao otimizador que o valor pode mudar de forma assíncrona,

volatile bool flag = false;

while (!flag) {
    /*do something*/
}

lerá sinalizador toda vez em volta do loop.

Se você desativar a otimização ou tornar todas as variáveis ​​voláteis, um programa se comportará da mesma maneira, mas mais lentamente. volátil significa apenas 'Eu sei que você pode ter acabado de ler e saber o que diz, mas se eu disser, leia, então leia.

O bloqueio faz parte do programa. Portanto, a propósito, se você estiver implementando semáforos, entre outras coisas, eles deverão ser voláteis. (Não tente, é difícil, provavelmente precisará de um pequeno montador ou do novo material atômico, e isso já foi feito.)

ctrl-alt-delor
fonte
1
Mas isso não é, e o mesmo exemplo na outra resposta, espera ocupada e, portanto, algo que deve ser evitado? Se este é um exemplo artificial, existem exemplos da vida real que não são inventados?
David Preston
7
@ Chris: Esperar muito tempo é ocasionalmente uma boa solução. Em particular, se você espera apenas esperar alguns ciclos de relógio, isso carrega muito menos sobrecarga do que a abordagem muito mais pesada de suspender o encadeamento. É claro que, como mencionei em outros comentários, exemplos como este são falhos porque eles assumem que as leituras / gravações na bandeira não serão reordenadas com relação ao código que ela protege, e não há garantia, e assim , volatilenão é realmente útil, mesmo neste caso. Mas a espera ocupada é uma técnica ocasionalmente útil.
jalf
3
@richard Sim e não. A primeira metade está correta. Mas isso significa apenas que a CPU e o compilador não podem reorganizar variáveis ​​voláteis em relação umas às outras. Se eu ler uma variável volátil A e, em seguida, ler uma variável volátil B, o compilador deverá emitir um código que é garantido (mesmo com a reordenação da CPU) para ler A antes de B. Mas não dá garantias sobre todos os acessos variáveis ​​não voláteis . Eles podem ser reordenados com base em sua leitura / gravação volátil. Portanto, a menos que você torne voláteis todas as variáveis ​​do seu programa, isso não lhe dará a garantia de que você está interessado.
jalf
2
@ Ctrl-Alt-Delor: Não é isso que volatilesignifica "não reordenar". Você espera que isso signifique que as lojas se tornem visíveis globalmente (para outros segmentos) na ordem do programa. Isso é o que atomic<T>com memory_order_releaseou seq_cstdá-lhe. Mas volatile apenas oferece a garantia de que não haja reordenação em tempo de compilação : cada acesso aparecerá no asm na ordem do programa. Útil para um driver de dispositivo. E útil para interação com um manipulador de interrupção, depurador ou manipulador de sinal no núcleo / encadeamento atual, mas não para interagir com outros núcleos.
Peter Cordes
1
volatilena prática, é suficiente para verificar um keep_runningsinalizador como você está fazendo aqui: CPUs reais sempre têm caches coerentes que não requerem liberação manual. Mas não há nenhuma razão para recomendar volatilemais atomic<T>com mo_relaxed; você terá o mesmo asm.
Peter Cordes