Este código trava no modo de liberação, mas funciona bem no modo de depuração


110

Eu me deparei com isso e quero saber o motivo desse comportamento no modo de depuração e liberação.

public static void Main(string[] args)
{            
   bool isComplete = false;

   var t = new Thread(() =>
   {
       int i = 0;

        while (!isComplete) i += 0;
   });

   t.Start();

   Thread.Sleep(500);
   isComplete = true;
   t.Join();
   Console.WriteLine("complete!");
}

25
qual é exatamente a diferença no comportamento?
Mong Zhu

4
Se fosse java, eu assumiria que o compilador não vê atualizações na variável 'compilar'. Adicionar 'volatile' à declaração da variável consertaria isso (e tornaria um campo estático).
Sebastian


4
Nota: é por isso que o devlopment multithread tem coisas como mutexes e operações atômicas. Quando você começa a usar vários segmentos, precisa considerar um conjunto notável de problemas de memória adicionais que não eram óbvios antes. Ferramentas de sincronização de threading como mutexes teriam resolvido isso.
Cort Ammon

5
@DavidSchwartz: Certamente isso é permitido . E é permitido que o compilador, o tempo de execução e a CPU façam com que o resultado desse código seja diferente do que você espera. Em particular, C # não tem permissão para tornar o acesso bool não atômico , mas é permitido mover leituras não voláteis para trás no tempo. Os duplos, ao contrário, não têm tal restrição à atomicidade; uma dupla leitura e escrita em dois threads diferentes sem sincronização pode ser interrompida.
Eric Lippert

Respostas:


149

Eu acho que o otimizador é enganado pela falta da palavra-chave 'volátil' na isCompletevariável.

Claro, você não pode adicioná-lo, porque é uma variável local. E, claro, como é uma variável local, não deveria ser necessária, porque os locais são mantidos na pilha e, naturalmente, estão sempre "atualizados".

No entanto , após a compilação, não é mais uma variável local . Como é acessado em um delegado anônimo, o código é dividido e traduzido em uma classe auxiliar e campo de membro, algo como:

public static void Main(string[] args)
{
    TheHelper hlp = new TheHelper();

    var t = new Thread(hlp.Body);

    t.Start();

    Thread.Sleep(500);
    hlp.isComplete = true;
    t.Join();
    Console.WriteLine("complete!");
}

private class TheHelper
{
    public bool isComplete = false;

    public void Body()
    {
        int i = 0;

        while (!isComplete) i += 0;
    }
}

Posso imaginar agora que o compilador / otimizador JIT em um ambiente multithread, ao processar a TheHelperclasse, pode realmente armazenarfalse em cache o valor em algum registro ou quadro de pilha no início do Body()método e nunca atualizá-lo até que o método termine. Isso porque não há NENHUMA GARANTIA de que o thread e o método NÃO terminarão antes que "= true" seja executado, então, se não houver garantia, por que não armazená-lo em cache e obter o aumento de desempenho ao ler o objeto heap uma vez em vez de lê-lo a cada iteração.

É exatamente por isso que a palavra-chave volatileexiste.

Para que esta classe auxiliar seja um pouco melhor corrigida 1) em ambientes multi-threaded, ela deve ter:

    public volatile bool isComplete = false;

mas, é claro, como é um código gerado automaticamente, você não pode adicioná-lo. Uma abordagem melhor seria adicionar alguns programas em lock()torno de leituras e gravações isCompleted, ou usar algum outro utilitário de sincronização ou threading / tasking pronto para usar, em vez de tentar fazê-lo bare-metal (o que não será bare-metal, já que é C # no CLR com GC, JIT e (..)).

A diferença no modo de depuração ocorre provavelmente porque no modo de depuração muitas otimizações são excluídas, então você pode, bem, depurar o código que você vê na tela. Portanto, while (!isComplete)não é otimizado para que você possa definir um ponto de interrupção lá e, portanto, isCompletenão é agressivamente armazenado em cache em um registro ou pilha no início do método e é lido do objeto no heap a cada iteração do loop.

BTW. Isso é apenas meu palpite sobre isso. Eu nem tentei compilá-lo.

BTW. Não parece ser um bug; é mais como um efeito colateral muito obscuro. Além disso, se eu estiver certo sobre isso, pode ser uma deficiência de linguagem - C # deve permitir colocar a palavra-chave 'volátil' em variáveis ​​locais que são capturadas e promovidas a campos de membro nos fechamentos.

1) veja abaixo os comentários de Eric Lippert sobre volatilee / ou este artigo muito interessante que mostra os níveis de complexidade envolvidos em garantir que o código que confia volatileseja seguro ... uh, bom ... uh, digamos OK.


2
@EricLippert: uau, muito obrigado por confirmar isso tão rápido! Como você acha, há alguma chance de que em alguma versão futura possamos obter uma volatileopção de variáveis ​​locais de captura para fechamento? Eu imagino que pode ser um pouco difícil de processar pelo compilador ..
quetzalcoatl

7
@quetzalcoatl: Eu não contaria com esse recurso sendo adicionado tão cedo. Esse é o tipo de codificação que você deseja desencorajar e não tornar mais fácil . Além disso, tornar as coisas voláteis não resolve necessariamente todos os problemas. Aqui está um exemplo em que tudo é volátil e o programa ainda está errado; você pode encontrar o bug? blog.coverity.com/2014/03/26/reordering-optimizations
Eric Lippert

3
Entendido. Eu desisto de tentar entender otimizações multithreading ... é insano como isso é complicado.
Entre

10
@Pikoh: Novamente, pense como um otimizador. Você tem uma variável que é incrementada, mas nunca lida. Uma variável que nunca é lida pode ser excluída totalmente.
Eric Lippert

4
@EricLippert agora minha mente deu um clique. Este tópico foi muito informativo, muito obrigado, de verdade.
Pikoh

82

A resposta do quetzalcoatl está correta. Para lançar mais luz sobre isso:

O compilador C # e o jitter CLR têm permissão para fazer muitas otimizações que assumem que o encadeamento atual é o único em execução. Se essas otimizações tornarem o programa incorreto em um mundo onde o thread atual não é o único thread em execução, isso é o seu problema . Você é obrigado a escrever programas multithread que dizem ao compilador e ao jitter que coisas malucas com multithreading você está fazendo.

Nesse caso particular, o jitter é permitido - mas não exigido - para observar que a variável não é alterada pelo corpo do loop e, portanto, concluir que - uma vez que, por suposição, este é o único thread em execução - a variável nunca mudará. Se nunca mudar, a variável precisa ser verificada quanto à verdade uma vez , não todas as vezes durante o loop. E é isso de fato o que está acontecendo.

Como resolver isso? Não escreva programas multithread . O multithreading é incrivelmente difícil de acertar, mesmo para especialistas. Se necessário, use os mecanismos de nível mais alto para atingir seu objetivo . A solução aqui não é tornar a variável volátil. A solução aqui é escrever uma tarefa cancelável e usar o mecanismo de cancelamento da Biblioteca Paralela de Tarefas . Deixe o TPL se preocupar em obter a lógica de encadeamento correta e o cancelamento ser enviado de maneira adequada pelos encadeamentos.


1
Os comentários não são para discussão extensa; esta conversa foi movida para o chat .
Madara's Ghost

14

Anexei ao processo de execução e descobri (se não cometi erros, não tenho muita prática com isso) que o Threadmétodo é traduzido para isto:

debug051:02DE04EB loc_2DE04EB:                            
debug051:02DE04EB test    eax, eax
debug051:02DE04ED jz      short loc_2DE04EB
debug051:02DE04EF pop     ebp
debug051:02DE04F0 retn

eax(que contém o valor de isComplete) é carregado pela primeira vez e nunca é atualizado.


8

Não é realmente uma resposta, mas para lançar um pouco mais de luz sobre o problema:

O problema parece ser quando ié declarada dentro do corpo lambda e é única lido na expressão de atribuição. Caso contrário, o código funciona bem no modo de lançamento:

  1. i declarado fora do corpo lambda:

    int i = 0; // Declared outside the lambda body
    
    var t = new Thread(() =>
    {
        while (!isComplete) { i += 0; }
    }); // Completes in release mode
  2. i não é lido na expressão de atribuição:

    var t = new Thread(() =>
    {
        int i = 0;
        while (!isComplete) { i = 0; }
    }); // Completes in release mode
  3. i também é lido em outro lugar:

    var t = new Thread(() =>
    {
        int i = 0;
        while (!isComplete) { Console.WriteLine(i); i += 0; }
    }); // Completes in release mode

Minha aposta é que algum compilador ou otimização JIT iestá bagunçando as coisas. Alguém mais esperto do que eu provavelmente será capaz de lançar mais luz sobre o assunto.

No entanto, eu não me preocuparia muito com isso, porque não consigo ver onde um código semelhante realmente serviria a algum propósito.


1
veja minha resposta, tenho quase certeza de que é tudo sobre a palavra-chave 'volátil' que não pode ser adicionada a uma variável local (que na verdade é promovida posteriormente a um campo de membro no fechamento) ..
quetzalcoatl
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.