Ilustrando o uso da palavra-chave volátil em C #

88

Eu gostaria de codificar um pequeno programa que ilustra visualmente o comportamento da volatilepalavra - chave. Idealmente, deve ser um programa que executa acesso simultâneo a um campo estático não volátil e que obtém um comportamento incorreto por causa disso.

Adicionar a palavra-chave volátil no mesmo programa deve resolver o problema.

Isso eu não consegui alcançar. Mesmo tentando várias vezes, habilitando a otimização, etc., sempre obtenho um comportamento correto sem a palavra-chave 'volátil'.

Você tem alguma ideia sobre esse assunto? Você sabe como simular esse problema em um aplicativo de demonstração simples? Depende de hardware?

Romain Verdier
fonte

Respostas:

102

Consegui um exemplo de trabalho!

A ideia principal recebida do wiki, mas com algumas alterações para C #. O artigo wiki demonstra isso para campos estáticos de C ++, parece que C # sempre compila cuidadosamente as solicitações para campos estáticos ... e faço um exemplo com um não estático:

Se você executar este exemplo no modo Release e sem o depurador (ou seja, usando Ctrl + F5), a linha while (test.foo != 255)será otimizada para 'while (true)' e este programa nunca retorna. Mas depois de adicionar a volatilepalavra-chave, você sempre obtém 'OK'.

class Test
{
    /*volatile*/ int foo;

    static void Main()
    {
        var test = new Test();

        new Thread(delegate() { Thread.Sleep(500); test.foo = 255; }).Start();

        while (test.foo != 255) ;
        Console.WriteLine("OK");
    }
}
S.Serpooshan
fonte
5
Testado em .NET 4.0, x86 e x64 - pode confirmar que o exemplo ainda é aplicável. Obrigado! :)
Roman Starkov
1
Isso é bom! Eu nunca soube que o JIT faz esse tipo de otimização e que o volátil o desativa.
usr
3
Apenas para ser explícito, você está adicionando a palavra-chave "volatile" à declaração de foo, certo?
JoeCool
21

Sim, é dependente do hardware (é improvável que você veja o problema sem vários processadores), mas também depende da implementação. As especificações do modelo de memória nas especificações CLR permitem coisas que a implementação do CLR da Microsoft não necessariamente faz.

Craig Stuntz
fonte
6

Não é realmente uma questão de falha acontecer quando a palavra-chave 'volatile' não é especificada, mais que um erro pode acontecer quando ela não foi especificada. Geralmente você saberá quando este for o caso melhor do que o compilador!

A maneira mais fácil de pensar sobre isso seria que o compilador poderia, se quisesse, embutir certos valores. Ao marcar o valor como volátil, você está dizendo a si mesmo e ao compilador que o valor pode realmente mudar (mesmo que o compilador não pense assim). Isso significa que o compilador não deve valores in-line, manter o cache ou ler o valor antecipadamente (na tentativa de otimizar).

Este comportamento não é realmente a mesma palavra-chave que em C ++.

O MSDN tem uma breve descrição aqui . Aqui está uma postagem talvez mais detalhada sobre os assuntos de Volatilidade, Atomicidade e Intertravamento

Ray Hayes
fonte
4

É difícil demonstrar em C #, já que o código é abstraído por uma máquina virtual, portanto, em uma implementação dessa máquina ele funciona corretamente sem voláteis, enquanto pode falhar em outra.

A Wikipedia tem um bom exemplo de como demonstrar isso em C, no entanto.

A mesma coisa poderia acontecer em C # se o compilador JIT decidir que o valor da variável não pode mudar de qualquer maneira e, portanto, cria um código de máquina que nem mesmo o verifica mais. Se agora outro encadeamento estava mudando o valor, seu primeiro encadeamento ainda pode estar preso no loop.

Outro exemplo é Busy Waiting.

Novamente, isso poderia acontecer com C # também, mas depende fortemente da máquina virtual e do compilador JIT (ou interpretador, se não tiver JIT ... em teoria, acho que o MS sempre usa um compilador JIT e também o Mono usa um; mas você pode desativá-lo manualmente).

Mecki
fonte
4

Aqui está minha contribuição para a compreensão coletiva deste comportamento ... Não é muito, apenas uma demonstração (baseada na demo do xkip) que mostra o comportamento de um verso volátil com um valor int não volátil (ou seja, "normal") lado a lado -lado, no mesmo programa ... que é o que eu estava procurando quando encontrei este tópico.

using System;
using System.Threading;

namespace VolatileTest
{
  class VolatileTest 
  {
    private volatile int _volatileInt;
    public void Run() {
      new Thread(delegate() { Thread.Sleep(500); _volatileInt = 1; }).Start();
      while ( _volatileInt != 1 ) 
        ; // Do nothing
      Console.WriteLine("_volatileInt="+_volatileInt);
    }
  }

  class NormalTest 
  {
    private int _normalInt;
    public void Run() {
      new Thread(delegate() { Thread.Sleep(500); _normalInt = 1; }).Start();
      // NOTE: Program hangs here in Release mode only (not Debug mode).
      // See: http://stackoverflow.com/questions/133270/illustrating-usage-of-the-volatile-keyword-in-c-sharp
      // for an explanation of why. The short answer is because the
      // compiler optimisation caches _normalInt on a register, so
      // it never re-reads the value of the _normalInt variable, so
      // it never sees the modified value. Ergo: while ( true )!!!!
      while ( _normalInt != 1 ) 
        ; // Do nothing
      Console.WriteLine("_normalInt="+_normalInt);
    }
  }

  class Program
  {
    static void Main() {
#if DEBUG
      Console.WriteLine("You must run this program in Release mode to reproduce the problem!");
#endif
      new VolatileTest().Run();
      Console.WriteLine("This program will now hang!");
      new NormalTest().Run();
    }

  }
}

Existem algumas explicações sucintas realmente excelentes acima, bem como algumas referências excelentes. Obrigado a todos por me ajudar a colocar minha cabeça ao redor volatile(pelo menos o suficiente para saber não confiar em volatileonde estava meu primeiro instinto lock).

Saúde e obrigado por TODOS os peixes. Keith.


PS: Eu estaria muito interessado em uma demonstração da solicitação original, que era: "Eu gostaria de ver um int estático volátil se comportando corretamente onde um int estático se comporta mal.

Eu tentei e falhei neste desafio. (Na verdade, desisti rapidamente ;-). Em tudo que tentei com vars estáticos, eles se comportaram "corretamente", independentemente de serem ou não voláteis ... e adoraria uma explicação de PORQUE é esse o caso, se de fato for o caso ... Será que o compilador não armazena em cache os valores de vars estáticos em registradores (ou seja, em vez disso, ele armazena em cache uma referência a esse endereço de pilha)?

Não, esta não é uma pergunta nova ... é uma tentativa de trazer a comunidade de volta à pergunta original.

Corlettk
fonte
2

Me deparei com o seguinte texto de Joe Albahari que me ajudou muito.

Peguei um exemplo do texto acima que alterei um pouco, criando um campo estático volátil. Quando você remove a volatilepalavra-chave, o programa bloqueia indefinidamente. Execute este exemplo no modo Release .

class Program
{
    public static volatile bool complete = false;

    private static void Main()
    {           
        var t = new Thread(() =>
        {
            bool toggle = false;
            while (!complete) toggle = !toggle;
        });

        t.Start();
        Thread.Sleep(1000); //let the other thread spin up
        complete = true;
        t.Join(); // Blocks indefinitely when you remove volatile
    }
}
Martijn B
fonte