Pergunta

Fui criado para acreditar que, se vários threads podem acessar uma variável, todas as leituras e gravações nessa variável devem ser protegidas por código de sincronização, como uma instrução "lock", porque o processador pode mudar para outro thread no meio do caminho. uma escrita.

No entanto, eu estava pesquisando System.Web.Security.Membership usando o Reflector e encontrei um código como este:

public static class Membership
{
    private static bool s_Initialized = false;
    private static object s_lock = new object();
    private static MembershipProvider s_Provider;

    public static MembershipProvider Provider
    {
        get
        {
            Initialize();
            return s_Provider;
        }
    }

    private static void Initialize()
    {
        if (s_Initialized)
            return;

        lock(s_lock)
        {
            if (s_Initialized)
                return;

            // Perform initialization...
            s_Initialized = true;
        }
    }
}

Por que o campo s_Initialized é lido fora do bloqueio?Outro thread não poderia estar tentando escrever nele ao mesmo tempo? As leituras e gravações de variáveis ​​são atômicas?

Foi útil?

Solução

Para a resposta definitiva, vá para as especificações.:)

Partição I, Seção 12.6.6 da especificação CLI afirma:"Uma CLI em conformidade garantirá que o acesso de leitura e gravação a locais de memória devidamente alinhados, não maiores que o tamanho da palavra nativa, seja atômico quando todos os acessos de gravação a um local forem do mesmo tamanho."

Isso confirma que s_Initialized nunca será instável e que leituras e gravações em tipos primitivos menores que 32 bits são atômicas.

Em particular, double e long (Int64 e UInt64) são não garantido ser atômico em uma plataforma de 32 bits.Você pode usar os métodos do Interlocked classe para protegê-los.

Além disso, embora leituras e gravações sejam atômicas, há uma condição de corrida com adição, subtração e incremento e decremento de tipos primitivos, uma vez que eles devem ser lidos, operados e reescritos.A classe intertravada permite protegê-los usando o CompareExchange e Increment métodos.

O intertravamento cria uma barreira de memória para evitar que o processador reordene leituras e gravações.O bloqueio cria a única barreira necessária neste exemplo.

Outras dicas

Esta é uma forma (ruim) do padrão de bloqueio de cheque duplo que é não thread seguro em C#!

Há um grande problema neste código:

s_Initialized não é volátil.Isso significa que as gravações no código de inicialização podem ser movidas após s_Initialized ser definido como true e outros threads podem ver o código não inicializado, mesmo que s_Initialized seja verdadeiro para eles.Isso não se aplica à implementação do Framework pela Microsoft porque cada gravação é uma gravação volátil.

Mas também na implementação da Microsoft, as leituras dos dados não inicializados podem ser reordenadas (ou seja,pré-buscado pela CPU), portanto, se s_Initialized for verdadeiro, a leitura dos dados que devem ser inicializados pode resultar na leitura de dados antigos e não inicializados devido a ocorrências de cache (ou seja,as leituras são reordenadas).

Por exemplo:

Thread 1 reads s_Provider (which is null)  
Thread 2 initializes the data  
Thread 2 sets s\_Initialized to true  
Thread 1 reads s\_Initialized (which is true now)  
Thread 1 uses the previously read Provider and gets a NullReferenceException

Mover a leitura de s_Provider antes da leitura de s_Initialized é perfeitamente legal porque não há leitura volátil em lugar nenhum.

Se s_Initialized fosse volátil, a leitura de s_Provider não teria permissão para se mover antes da leitura de s_Initialized e também a inicialização do Provedor não teria permissão para se mover depois que s_Initialized fosse definido como verdadeiro e tudo estivesse bem agora.

Joe Duffy também escreveu um artigo sobre este problema: Variantes quebradas no bloqueio verificado duas vezes

Espere aí – a pergunta que está no título definitivamente não é a verdadeira pergunta que Rory está fazendo.

A pergunta titular tem a resposta simples de “Não” – mas isso não ajuda em nada, quando você vê a pergunta real – para a qual não acho que alguém tenha dado uma resposta simples.

A verdadeira questão que Rory faz é apresentada muito mais tarde e é mais pertinente ao exemplo que ele dá.

Por que o campo S_Initialized é lido fora da fechadura?

A resposta para isso também é simples, embora completamente alheia à atomicidade do acesso a variáveis.

O campo s_Initialized é lido fora do bloqueio porque fechaduras são caras.

Como o campo s_Initialized é essencialmente "gravar uma vez", ele nunca retornará um falso positivo.

É econômico lê-lo fora da fechadura.

Isto é um baixo custo atividade com um alto chance de ter um benefício.

É por isso que é lido fora da fechadura – para evitar pagar o custo do uso de uma fechadura, a menos que seja indicado.

Se as fechaduras fossem baratas, o código seria mais simples e omitiria a primeira verificação.

(editar:Segue-se uma boa resposta de Rory.Sim, leituras booleanas são muito atômicas.Se alguém construísse um processador com leituras booleanas não atômicas, ele seria apresentado no DailyWTF.)

A resposta correta parece ser: “Sim, principalmente”.

  1. A resposta de John referenciando a especificação CLI indica que os acessos a variáveis ​​não maiores que 32 bits em um processador de 32 bits são atômicos.
  2. Confirmação adicional da especificação C#, seção 5.5, Atomicidade de referências variáveis:

    As leituras e gravações dos seguintes tipos de dados são atômicas:bool, char, byte, sbyte, short, ushort, uint, int, float e tipos de referência.Além disso, leituras e gravações de tipos enum com um tipo subjacente na lista anterior também são atômicas.Leituras e gravações de outros tipos, incluindo long, ulong, double e decimal, bem como tipos definidos pelo usuário, não têm garantia de serem atômicas.

  3. O código no meu exemplo foi parafraseado da classe Membership, conforme escrito pela própria equipe do ASP.NET, então sempre foi seguro assumir que a forma como ele acessa o campo s_Initialized está correta.Agora sabemos por quê.

Editar:Como aponta Thomas Danecker, mesmo que o acesso do campo seja atômico, s_Initialized deveria realmente ser marcado volátil para garantir que o bloqueio não seja quebrado pelo processador reordenando as leituras e gravações.

A função Inicializar está com defeito.Deveria ficar mais assim:

private static void Initialize()
{
    if(s_initialized)
        return;

    lock(s_lock)
    {
        if(s_Initialized)
            return;
        s_Initialized = true;
    }
}

Sem a segunda verificação dentro da fechadura é possível que o código de inicialização seja executado duas vezes.Portanto, a primeira verificação é para o desempenho, para evitar que você tenha um bloqueio desnecessário, e a segunda verificação é para o caso em que um thread está executando o código de inicialização, mas ainda não definiu o s_Initialized flag e assim um segundo thread passaria na primeira verificação e ficaria aguardando no bloqueio.

As leituras e gravações de variáveis ​​não são atômicas.Você precisa usar APIs de sincronização para emular leituras/gravações atômicas.

Para uma referência incrível sobre isso e muitos outros problemas relacionados à simultaneidade, certifique-se de pegar uma cópia do livro de Joe Duffy último espetáculo.É um estripador!

"Acessar uma variável em C# é uma operação atômica?"

Não.E não é coisa de C#, nem de .net, é coisa de processador.

OJ está certo de que Joe Duffy é o cara a quem recorrer para obter esse tipo de informação.E "intertravado" é um ótimo termo de pesquisa para usar se você quiser saber mais.

"Leituras interrompidas" podem ocorrer em qualquer valor cujos campos somam mais do que o tamanho de um ponteiro.

@Leão
Entendo o que você quer dizer - da maneira como perguntei e depois comentei, a pergunta permite que ela seja interpretada de duas maneiras diferentes.

Para ser claro, eu queria saber se era seguro ter threads simultâneos lendo e escrevendo em um campo booleano sem nenhum código de sincronização explícito, ou seja, acessando uma variável booleana (ou outro tipo primitivo) atômica.

Em seguida, usei o código Membership para dar um exemplo concreto, mas isso introduziu um monte de distrações, como o bloqueio de verificação dupla, o fato de que s_Initialized só é definido uma vez e comentei o próprio código de inicialização.

Meu erro.

Você também pode decorar s_Initialized com a palavra-chave volátil e renunciar totalmente ao uso de lock.

Isso não está correto.Você ainda encontrará o problema de um segundo thread passar na verificação antes que o primeiro thread tenha a chance de definir o sinalizador, o que resultará em múltiplas execuções do código de inicialização.

Acho que você está perguntando se s_Initialized pode estar em um estado instável quando lido fora da fechadura.A resposta curta é não.Uma simples atribuição/leitura se resumirá a uma única instrução de montagem que é atômica em todos os processadores que consigo imaginar.

Não tenho certeza de qual é o caso da atribuição a variáveis ​​de 64 bits, depende do processador, eu diria que não é atômico, mas provavelmente está em processadores modernos de 32 bits e certamente em todos os processadores de 64 bits.A atribuição de tipos de valores complexos não será atômica.

Eu pensei que sim - não tenho certeza do objetivo do bloqueio no seu exemplo, a menos que você também esteja fazendo algo com s_Provider ao mesmo tempo - então o bloqueio garantiria que essas chamadas acontecessem juntas.

Isso //Perform initialization capa de comentário criando s_Provider?Por exemplo

private static void Initialize()
{
    if (s_Initialized)
        return;

    lock(s_lock)
    {
        s_Provider = new MembershipProvider ( ... )
        s_Initialized = true;
    }
}

Caso contrário, essa propriedade estática retornará nulo de qualquer maneira.

Talvez Intertravado dá uma pista.E caso contrário Este eu sou muito bom.

Eu teria adivinhado que eles não eram atômicos.

Para fazer seu código sempre funcionar em arquiteturas fracamente ordenadas, você deve colocar um MemoryBarrier antes de escrever s_Initialized.

s_Provider = new MemershipProvider;

// MUST PUT BARRIER HERE to make sure the memory writes from the assignment
// and the constructor have been wriitten to memory
// BEFORE the write to s_Initialized!
Thread.MemoryBarrier();

// Now that we've guaranteed that the writes above
// will be globally first, set the flag
s_Initialized = true;

Não há garantia de que as gravações de memória que acontecem no construtor MembershipProvider e as gravações em s_Provider aconteçam antes de você gravar em s_Initialized em um processador com ordem fraca.

Muita reflexão neste tópico é sobre se algo é atômico ou não.Esse não é o problema.A questão é a ordem em que as gravações do seu tópico ficam visíveis para outros tópicos.Em arquiteturas fracamente ordenadas, as gravações na memória não ocorrem em ordem e ESSE é o verdadeiro problema, não se uma variável cabe no barramento de dados.

EDITAR: Na verdade, estou misturando plataformas nas minhas declarações.Em C#, a especificação CLR exige que as gravações sejam globalmente visíveis e em ordem (usando instruções caras de armazenamento para cada armazenamento, se necessário).Portanto, você não precisa realmente ter aquela barreira de memória ali.No entanto, se fosse C ou C++, onde não existe tal garantia de ordem de visibilidade global, e sua plataforma de destino pudesse ter memória fracamente ordenada e fosse multithread, então você precisaria garantir que as gravações dos construtores sejam globalmente visíveis antes de atualizar s_Initialized , que é testado fora do bloqueio.

Um If (itisso) { Verificar um booleano é atômico, mas mesmo que não tenha sido necessário bloquear a primeira verificação.

Se algum thread tiver concluído a inicialização, isso será verdade.Não importa se vários threads estão verificando ao mesmo tempo.Todos obterão a mesma resposta e não haverá conflito.

A segunda verificação dentro do bloqueio é necessária porque outro thread pode ter capturado o bloqueio primeiro e já concluído o processo de inicialização.

O que você está perguntando é se está acessando um campo em um método várias vezes atômico - para o qual a resposta é não.

No exemplo acima, a rotina de inicialização está com defeito, pois pode resultar em inicializações múltiplas.Você precisaria verificar o s_Initialized sinalizador dentro e fora do bloqueio, para evitar uma condição de corrida na qual vários threads leem o s_Initialized flag antes de qualquer um deles realmente executar o código de inicialização.Por exemplo.,

private static void Initialize()
{
    if (s_Initialized)
        return;

    lock(s_lock)
    {
        if (s_Initialized)
            return;
        s_Provider = new MembershipProvider ( ... )
        s_Initialized = true;
    }
}

Ack, deixa pra lá...como apontado, isso é realmente incorreto.Isso não impede que um segundo thread entre na seção de código "inicializar".Bah.

Você também pode decorar s_Initialized com a palavra-chave volátil e renunciar totalmente ao uso de lock.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top