Pergunta

Eu ainda estou um pouco claro e quando para embrulhar um Bloqueio em torno de algum código. Minha regra de polegar geral é para embrulhar uma operação em um bloqueio quando se lê ou escreve uma variável estática. Mas quando uma variável estática é somente leitura (por exemplo, é um de somente leitura que foi ajustada durante tipo de inicialização), o acesso a ele não precisa de ser envolvido em uma declaração de bloqueio, certo? Recentemente, vi um código que parecia ser o exemplo a seguir, e isso fez-me pensar que pode haver algumas lacunas no meu conhecimento multithreading:

class Foo
{
    private static readonly string bar = "O_o";

    private bool TrySomething()
    {
        string bar;

        lock(Foo.objectToLockOn)
        {
            bar = Foo.bar;          
        }       

        // Do something with bar
    }
}

Isso simplesmente não faz sentido para mim? - Por que haveria por problemas de simultaneidade com a leitura de um registo

Além disso, este exemplo traz à tona uma outra questão. É um destes melhor que o outro? (Por exemplo, exemplo dois mantém o bloqueio por menos tempo?) Acho que eu poderia desmontar o MSIL ...

class Foo
{
    private static string joke = "yo momma";

    private string GetJoke()
    {
        lock(Foo.objectToLockOn)
        {
            return Foo.joke;
        }
    }
}

vs.

class Foo
{
    private static string joke = "yo momma";

        private string GetJoke()
        {
            string joke;

            lock(Foo.objectToLockOn)
            {
                joke = Foo.joke;
            }

            return joke;
        }
}
Foi útil?

Solução

Uma vez que nenhum do código que você escreveu modifica o campo estático após a inicialização, não há necessidade de qualquer bloqueio. Apenas substituindo a corda com um novo valor não vai precisar de sincronização seja, a menos que o novo valor depende dos resultados de uma leitura do valor antigo.

Os campos estáticos não são as únicas coisas que a sincronização de necessidade, qualquer referência compartilhada que pode ser modificado é vulnerável a problemas de sincronização.

class Foo
{
    private int count = 0;
    public void TrySomething()    
    {
        count++;
    }
}

Você pode supor que dois threads em execução o método TrySomething seria ótimo. Mas não é.

  1. Thread A lê o valor de contagem (0) em um registro para que possa ser incrementado.
  2. mudança de contexto! O Agendador de thread decide thread A teve tempo de execução suficiente. Em seguida na linha é o segmento B.
  3. Tópico B lê o valor de contagem (0) em um registro.
  4. incrementos segmento B do registo.
  5. Tópico B salva o resultado (1) para contar.
  6. mudança de contexto de volta para A.
  7. recarrega segmento à registrar com o valor de contagem (0) salvos na sua pilha.
  8. incrementos fio um registo.
  9. Thread A salva o resultado (1) para contar.

Assim, mesmo que chamamos contagem ++ duas vezes, o valor de contagem foi apenas de 0 a 1. Vamos fazer thread-safe o código:

class Foo
{
    private int count = 0;
    private readonly object sync = new object();
    public void TrySomething()    
    {
        lock(sync)
            count++;
    }
}

Agora, quando segmento A é interrompido Tópico B não pode mexer com a contagem, porque ele vai bater a declaração de bloqueio e, em seguida, bloquear até Thread A lançou sincronização.

A propósito, há uma maneira alternativa de fazer incrementar Int32s e Int64s thread-safe:

class Foo
{
    private int count = 0;
    public void TrySomething()    
    {
        System.Threading.Interlocked.Increment(ref count);
    }
}

Em relação à segunda parte da sua pergunta, eu acho que vou apenas ir com o que for mais fácil de ler, qualquer diferença de desempenho não será insignificante. otimização precoce é a raiz de todo mal, etc.

Por threading é difícil

Outras dicas

Reading ou escrever um campo de 32 bits ou menor é uma operação atômica em C #. Não há necessidade de um bloqueio no código que você apresentou, tanto quanto eu posso ver.

Parece-me que o bloqueio é desnecessário em seu primeiro caso. Usando o inicializador estático para inicializar bar é garantido para ser thread-safe. Desde que você sempre apenas ler o valor, não há nenhuma necessidade para travá-lo. Se o valor não vai mudar, nunca haverá qualquer disputa, por bloqueio em tudo?

sujo lê?

Na minha opinião, você deve tentar arduamente para não colocar variáveis ??estáticas em uma posição onde eles precisam ser de leitura / escrita a partir de diferentes tópicos. Eles são essencialmente livre para todas as variáveis ??globais, nesse caso, e globals são quase sempre uma coisa má.

Dito isto, se você colocar uma variável estática em tal posição, você pode querer bloqueio durante uma leitura, apenas no caso - lembre-se, outro segmento pode ter mergulhou dentro e alterado o valor durante a leitura, e se isso acontecer, você pode acabar com dados corrompidos. Lê-se de operações não necessariamente atômicas a menos que você garantir que eles são de bloqueio. Mesmo com as gravações - elas não são sempre operações atômicas quer

.

Edit: Como Mark apontou, para certos primitivos em C # lê são sempre atômica. Mas tenha cuidado com outros tipos de dados.

Se você está apenas escrever um valor para um ponteiro, você não precisa de bloqueio, uma vez que a ação é atômica. Geralmente, você deve bloquear qualquer momento que você precisa fazer uma transacção que envolva pelo menos duas ações atômicas (lê ou grava) que dependem do Estado não muda entre o começo eo fim.

Dito isto - Eu venho de terra Java, onde todas as leituras e gravações de variáveis ??são ações atômicas. Outras respostas aqui sugerem que .NET é diferente.

Quanto ao seu "o que é melhor" a questão, eles são a mesma desde o escopo de função não é usada para qualquer outra coisa.

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