Pergunta

Se eu tiver um tipo profundamente imutável (todos os membros são somente leitura e se eles são membros de tipo de referência, então eles também se referem a objetos que estão profundamente imutável).

Gostaria de implementar uma propriedade inicializado preguiçoso do tipo, como este:

private ReadOnlyCollection<SomeImmutableType> m_PropName = null;
public ReadOnlyCollection<SomeImmutableType> PropName
{
    get
    {
        if(null == m_PropName)
        {
            ReadOnlyCollection<SomeImmutableType> temp = /* do lazy init */;
            m_PropName = temp;
        }
        return m_PropName;
    }
}

De que eu posso dizer:

m_PropName = temp; 

... é threadsafe. Eu não sou preocupado demasiado sobre dois tópicos tanto corrida para inicializar, ao mesmo tempo, porque vai ser raro, ambos os resultados seriam idênticos do ponto de vista lógico, e eu prefiro não usar um bloqueio, se eu não tenho a.

Will este trabalho? Quais são os prós e contras?

Editar: Obrigado por suas respostas. Eu provavelmente vai avançar com o uso de um bloqueio. No entanto, estou surpreso que ninguém levantou a possibilidade do compilador percebendo que a variável temporário é desnecessária, e apenas atribuir direto para m_PropName. Se fosse esse o caso, então um segmento de leitura poderia ler um objeto que não tenha terminado sendo construído. O compilador evitar tal situação, um?

(As respostas parecem indicar que o tempo de execução não vai permitir que isso aconteça.)

Editar: Então, eu decidi ir com um método Interlocked CompareExchange inspirado por este artigo Joe Duffy .

Basicamente:

private ReadOnlyCollection<SomeImmutableType> m_PropName = null;
public ReadOnlyCollection<SomeImmutableType> PropName
{
    get
    {
        if(null == m_PropName)
        {
            ReadOnlyCollection<SomeImmutableType> temp = /* do lazy init */;
            System.Threading.Interlocked(ref m_PropName, temp, null);
        }
        return m_PropName;
    }
}

Isto é suposto para garantir que os tópicos que chamam este método nesta instância objeto irá obter uma referência para o mesmo objeto, de modo que o operador == vai funcionar. É possível ter o trabalho desperdiçado, o que é bom -. Ele só faz isso um algoritmo otimista

Como observado no abaixo alguns comentários, isso depende do modelo de memória .NET 2.0 para trabalho. Caso contrário, m_PropName deve ser declarado volátil.

Foi útil?

Solução

Isso vai funcionar. Escrevendo para referências em C # é garantido para ser atômica, como descrito na seção 5.5 do spec. Isso ainda não é provavelmente uma boa maneira de fazê-lo, porque seu código será mais confuso para depuração e ler em troca de um efeito provavelmente menor em desempenho.

Jon Skeet tem uma grande página na implementação singeltons em C #.

O conselho geral sobre pequenas otimizações como estes não é para fazê-las a menos que um profiler diz que este código é um hotspot. Além disso, você deve ser cauteloso de escrever código que não pode ser totalmente compreendido pela maioria dos programadores sem verificar o spec.

EDIT: Como observado nos comentários, mesmo que você diz que não se importa se 2 versões de seu objeto são criados, essa situação é tão contra-intuitivo que esta abordagem não deve ser usado.

Outras dicas

Você deve usar um bloqueio. Caso contrário, corre o risco de duas instâncias de m_PropName existente e em uso por diferentes threads. Isto pode não ser um problema em muitos casos; No entanto, se você quer ser capaz de usar == vez de .equals() então isso vai ser um problema. condições de corrida raras não são o bug melhor ter. Eles são difíceis de depurar e reproduzir-se.

Em seu código, se dois tópicos diferentes simultaneamente recebem seu PropName propriedade (digamos, em uma CPU multi-core), então eles podem receber diferentes novas instâncias da propriedade que irá conter dados idênticos, mas não ser a mesma instância do objeto.

Um dos principais benefícios de objetos imutáveis ??é que == é equivalente a .equals(), permitindo o uso do == maior performance para comparação. Se você não sincronizar na inicialização lenta, então o risco de perder esse benefício.

Você também imutabilidade perder. Seu objeto será inicializado duas vezes com diferentes objetos (que contêm os mesmos valores), então um segmento que já tem o valor de sua propriedade, mas que recebe-lo novamente, pode receber um objeto diferente na segunda vez.

Eu estaria interessado em ouvir outras respostas para isso, mas eu não vejo um problema com ele. A cópia duplicada será abandonado e recebe GCed.

Você precisa fazer o volatile campo embora.

Em relação a este:

No entanto, estou surpreso que ninguém trouxe -se a possibilidade do compilador percebendo que a variável é temporário desnecessário, e apenas atribuindo direto para m_PropName. Se isso fosse o caso, então um segmento de leitura poderia possivelmente ler um objeto que não tem acabado a ser construída. Será que o compilador evitar tal situação, um?

Eu considerei mencioná-lo, mas não faz diferença. O novo operador não retornar uma referência (e assim a atribuição ao campo não acontece) até as concluída construtor -. Isso é garantido pelo tempo de execução, não o compilador

No entanto, a linguagem / tempo de execução não realmente garantir que outros segmentos não é possível ver um objeto parcialmente construído - depende do que o construtor faz .

Update:

O OP também se pergunta se esta página tem uma ideia útil . Seu trecho de código final é uma instância de dobro verificado que é o exemplo clássico de uma idéia que bloqueio milhares de pessoas recommmend para o outro sem qualquer ideia de como fazê-lo direito. O problema é que máquinas SMP consistem em várias CPUs com os seus próprios caches de memória. Se eles tiveram que sincronizar seus caches cada vez que havia uma atualização de memória, isso iria desfazer os benefícios de ter várias CPUs. Então, eles só sincronizar em uma "barreira de memória", que ocorre quando um bloqueio é retirado, ou uma operação intertravado ocorre, ou uma variável volatile é acessado.

A ordem habitual de eventos é:

  • Coder descobre verifiquei bloqueio
  • Coder descobre barreiras de memória

Entre esses dois eventos, eles liberam uma grande quantidade de software quebrado.

Além disso, muitas pessoas acreditam (como aquele cara faz) que você pode "eliminar bloqueio" usando operações interligadas. Mas em tempo de execução são uma barreira de memória e assim eles causam todas as CPUs para parar e sincronizar seus caches. Eles têm uma vantagem sobre os bloqueios na medida em que não precisa fazer uma chamada para o kernel do sistema operacional (eles são "o código do usuário" apenas), mas eles podem matar o desempenho tanto quanto qualquer técnica de sincronização .

Resumo:. Enfiando olhares de código aproximadamente 1000 x mais fácil escrever do que é

Eu sou todo para o init preguiçoso quando os dados nem sempre pode ser acessado e pode levar uma boa quantidade de recursos para buscar ou armazenar os dados.

Eu acho que existe um conceito-chave a ser esquecido aqui: De acordo com a C # conceitos de design, você não deve fazer seus membros de instância thread-safe por padrão Apenas membros estáticos devem ser feitas thread-safe por padrão.. A menos que você está acessando alguns dados estáticos / globais, você não deve adicionar fechaduras extras em seu código.

Pelo que os seus programas de código, init preguiçoso é tudo dentro de uma propriedade de instância, então eu não gostaria de acrescentar bloqueios para ele. Se, pelo projeto, que se destina a ser acessado por vários threads simultaneamente, então vá em frente e adicione o bloqueio.

By the way, ele pode não reduzir o código por muito, mas eu sou fã do operador nulo coalesce. O corpo ao seu getter poderia tornar-se este em vez disso:
m_PropName = m_PropName ?? new ...();
return m_PropName;

Ele se livrar da "if (m_PropName == null) ..." extra e em minha opinião torna mais conciso e legível.

Eu não sou nenhum expert # C, mas tanto quanto eu posso dizer, isso só coloca um problema se você precisar que apenas uma instância de ReadOnlyCollection é criado. Você diz que o objeto criado será sempre o mesmo e não importa se duas (ou mais) threads fazer criar uma nova instância, então eu diria que é ok fazer isso sem um bloqueio.

Uma coisa que pode tornar-se um bug estranho mais tarde seria se se poderia comparar a igualdade dos casos, o que por vezes não seria o mesmo. Mas se você manter isso em mente (ou simplesmente não fazer isso) Eu não vejo outros problemas.

Infelizmente, você precisa de um bloqueio. Há um monte de erros muito sutis quando você não bloquear correctamente. Para um exemplo de olhar assustador no esta resposta href="https://stackoverflow.com/questions/9666/is-accessing-a-variable-in-c-an-atomic-operation/73827#73827"> .

Pode-se usar com segurança a inicialização lenta, sem um bloqueio se o campo só será escrita se for em branco ou detém já seja o valor a ser escrito ou, em alguns casos, um equivalente . Note-se que há dois objetos mutáveis ??são equivalentes; um campo que contém uma referência a um objeto mutável só pode ser escrito com uma referência ao o mesmo objeto (ou seja, a gravação não teria nenhum efeito).

Existem três padrões gerais pode-se usar para a inicialização lenta, dependendo das circunstâncias:

  1. Use uma fechadura se calcular o valor de escrever seria caro, e se deseja evitar despender um esforço tão desnecessariamente. O padrão de bloqueio duplo verificado é bom em sistemas cujas suporta modelo de memória.
  2. Se um está armazenando um valor imutável, calcular-lo se ele parece ser necessário, e apenas armazená-lo. Outros tópicos que não vêem a loja pode executar uma computação redundante, mas eles simplesmente tentar escrever o campo com o valor que já está lá.
  3. Se um está armazenando uma referência a um objeto de classe mutável barato-to-produzir, criar um novo objeto se ele parece ser necessário, e depois usar o `Interlocked.CompareExchange` para armazená-lo se o campo é ainda em branco.

Note que, se pode-se evitar o bloqueio em qualquer acesso que não seja a primeira dentro de um tópico, tornando o preguiçoso leitor thread-safe não deve impor qualquer custo significativo desempenho. Embora seja comum para as classes mutáveis ??não ser thread-safe, todas as classes que afirmam ser imutável deve ser 100% thread-safe para qualquer combinação de ações leitor. Qualquer classe que não pode cumprir tal exigência thread-segurança não deve pretender ser imutável.

Este é definitivamente um problema.

Considere este cenário: Linha "A" acessa a propriedade, ea coleção é inicializado. Antes ele atribui a instância local para o campo "m_PropName", Thread "B" acessa a propriedade, exceto que ele recebe para completar. Thread "B" agora tem uma referência a essa instância, que atualmente está armazenado em "m_PropName" ... até segmento "A" continua, altura em que "m_PropName" é substituída pela instância local em que segmento.

Há agora um par de problemas. Primeiro, Linha "B" não tem a instância correta mais, uma vez que o objeto proprietário pensa que "m_PropName" é a única instância, no entanto, vazou uma instância inicializado quando Tópico "B" concluída antes da linha "A". Outra é se a coleção mudou entre o momento da linha "A" e linha "B" tem suas instâncias. Então você tem dados incorretos. Pode até ser pior se você estivesse observando ou modificar a coleção somente leitura internamente (o que, naturalmente, você não pode com ReadOnlyCollection, mas poderia se você substituí-lo com alguma outra aplicação que você pode observar através de eventos ou modificar internamente, mas não externamente).

scroll top