O que torna um método seguro para threads? Quais são as regras?

156

Existem regras / diretrizes gerais para o que torna um método seguro para threads? Entendo que provavelmente há um milhão de situações pontuais, mas e em geral? Isso é simples?

  1. Se um método acessa apenas variáveis ​​locais, é seguro para threads.

É isso? Isso também se aplica a métodos estáticos?

Uma resposta, fornecida por @Cybis, foi:

Variáveis ​​locais não podem ser compartilhadas entre os segmentos, porque cada segmento recebe sua própria pilha.

Esse também é o caso dos métodos estáticos?

Se um método é passado para um objeto de referência, isso quebra a segurança do encadeamento? Eu fiz algumas pesquisas e existem muitos casos sobre certos casos, mas eu esperava poder definir, usando apenas algumas regras, diretrizes a seguir para garantir que um método seja seguro para threads.

Então, acho que minha pergunta final é: "Existe uma pequena lista de regras que definem um método seguro para threads? Em caso afirmativo, quais são elas?"

EDIT
Muitos pontos positivos foram feitos aqui. Penso que a resposta real para esta pergunta é: "Não existem regras simples para garantir a segurança do thread". Legal. Bem. Mas, em geral , acho que a resposta aceita fornece um resumo breve e bom. Sempre há exceções. Que assim seja. Eu posso viver com isso.

Bob Horn
fonte
59
Você não deve acessar variáveis ​​que também são acessadas por outros threads sem um locketh.
Hans Passant
4
Hanth pathant se transformou em um igor!
Martin James
3
Também .. 'Você não deve acessar variáveis ​​que também são acessadas por outros threads sem bloqueio' - ao contrário do que importa, se o valor lido ocasionalmente não for o mais recente ou estiver incorretamente incorreto.
Martin James
Aqui está um bom blog de Eric para levá-lo a um turbilhão.
RBT 06/06

Respostas:

140

Se um método (instância ou estático) referenciar apenas variáveis ​​com escopo dentro desse método, ele será seguro para threads, pois cada thread possui sua própria pilha:

Nesse caso, vários threads poderiam chamar ThreadSafeMethodsimultaneamente sem problemas.

public class Thing
{
    public int ThreadSafeMethod(string parameter1)
    {
        int number; // each thread will have its own variable for number.
        number = parameter1.Length;
        return number;
    }
}

Isso também é verdade se o método chamar outro método de classe que faça referência apenas a variáveis ​​com escopo local:

public class Thing
{
    public int ThreadSafeMethod(string parameter1)
    {
        int number;
        number = this.GetLength(parameter1);
        return number;
    }

    private int GetLength(string value)
    {
        int length = value.Length;
        return length;
    }
}

Se um método acessar propriedades ou campos (estado do objeto) (instância ou estática), será necessário usar bloqueios para garantir que os valores não sejam modificados por um encadeamento diferente.

public class Thing
{
    private string someValue; // all threads will read and write to this same field value

    public int NonThreadSafeMethod(string parameter1)
    {
        this.someValue = parameter1;

        int number;

        // Since access to someValue is not synchronised by the class, a separate thread
        // could have changed its value between this thread setting its value at the start 
        // of the method and this line reading its value.
        number = this.someValue.Length;
        return number;
    }
}

Você deve estar ciente de que quaisquer parâmetros passados ​​para o método que não sejam uma estrutura ou imutáveis ​​podem ser alterados por outro encadeamento fora do escopo do método.

Para garantir a simultaneidade adequada, você precisa usar o bloqueio.

para obter mais informações, consulte a referência de bloqueio C # reference e ReadWriterLockSlim .

O bloqueio é útil principalmente para fornecer uma funcionalidade de cada vez;
ReadWriterLockSlimé útil se você precisar de vários leitores e escritores únicos.

Trevor Pilley
fonte
15
No terceiro exemplo, private string someValue;não é staticassim que cada instância receberá uma cópia separada dessa variável. Então, você pode explicar como isso não é seguro para threads?
precisa
29
@Bharadwaj se houver uma instância da Thingclasse acessada por vários segmentos
Trevor Pilley
112

Se um método acessa apenas variáveis ​​locais, é seguro para threads. É isso?

Absolutamente não. Você pode escrever um programa com apenas uma única variável local acessada a partir de um único thread que, no entanto, não é seguro:

https://stackoverflow.com/a/8883117/88656

Isso também se aplica a métodos estáticos?

Absolutamente não.

Uma resposta, fornecida pelo @Cybis, foi: "Variáveis ​​locais não podem ser compartilhadas entre os segmentos, porque cada segmento recebe sua própria pilha".

Absolutamente não. A característica distintiva de uma variável local é que ela é visível no escopo local , e não que seja alocada no pool temporário . É perfeitamente legal e possível acessar a mesma variável local a partir de dois threads diferentes. Você pode fazer isso usando métodos anônimos, lambdas, blocos de iteradores ou métodos assíncronos.

Esse também é o caso dos métodos estáticos?

Absolutamente não.

Se um método é passado para um objeto de referência, isso quebra a segurança do encadeamento?

Talvez.

Eu fiz algumas pesquisas e existem muitos casos sobre certos casos, mas eu esperava poder definir, usando apenas algumas regras, diretrizes a seguir para garantir que um método seja seguro para threads.

Você terá que aprender a viver com decepção. Este é um assunto muito difícil.

Então, acho que minha pergunta final é: "Existe uma pequena lista de regras que definem um método seguro para threads?

Não. Como você viu no meu exemplo anterior, um método vazio pode não ser seguro para threads . Você também pode perguntar "existe uma pequena lista de regras que garantem que um método esteja correto ". Não, não há. A segurança da linha nada mais é do que um tipo de correção extremamente complicado.

Além disso, o fato de você estar fazendo a pergunta indica seu mal-entendido fundamental sobre segurança de threads. A segurança do thread é uma propriedade global e não local de um programa. A razão pela qual é tão difícil acertar é porque você deve ter um conhecimento completo do comportamento de segmentação de todo o programa para garantir sua segurança.

Novamente, veja o meu exemplo: todo método é trivial . É a maneira como os métodos interagem entre si em um nível "global" que torna o programa em conflito. Você não pode olhar para todos os métodos e selecioná-lo como "seguro" e esperar que todo o programa seja seguro, assim como não é possível concluir que, porque sua casa é feita de tijolos 100% não vazados, a casa também é não oco. O vazio de uma casa é uma propriedade global de toda a coisa, não um agregado das propriedades de suas partes.

Eric Lippert
fonte
15
Declaração central: a segurança do encadeamento é uma propriedade global e não local de um programa.
Greg D
5
@BobHorn: class C { public static Func<int> getter; public static Action<int> setter; public static void M() { int x = 0; getter = ()=>x; setter = y=>{x=y;};} } Chame M () e chame C.getter e C.setter em dois threads diferentes. A variável local agora pode ser gravada e lida em dois threads diferentes, mesmo que seja local. Novamente: a característica definidora de uma variável local é que ela é local , não que esteja na pilha do encadeamento .
Eric Lippert 24/03
3
@ BobHorn: Eu acho que tem mais a ver com a compreensão de nossas ferramentas em um nível que permita que possamos falar sobre elas com autoridade e conhecimento. Por exemplo, a pergunta original traiu a falta de entendimento sobre o que é uma variável local. Este erro é bastante comum, mas o fato é que é um erro e deve ser corrigido. A definição de uma variável local é bastante precisa e deve ser tratada de acordo. :) Esta não é uma resposta para "pessoas" que não entendem que existem casos patológicos. Estes são um esclarecimento para que você possa entender o que realmente pediu. :)
Greg D
7
@EricLippert: A documentação do MSDN para muitas classes declara que 'Membros estáticos públicos (compartilhados no Visual Basic) desse tipo são seguros para threads. Não é garantido que nenhum membro da instância seja seguro para threads. ' ou às vezes 'Esse tipo é seguro para threads'. (por exemplo, String), como essas garantias podem ser feitas quando a segurança de threads é uma preocupação global?
Mike Zboray
3
@mikez: Boa pergunta. Esses métodos não modificam nenhum estado ou, quando o fazem, modificam o estado com segurança sem bloqueios ou, se eles usam bloqueios, o fazem de uma maneira que garanta que a "ordem de bloqueio" global não seja violada. Strings são estruturas de dados imutáveis ​​cujos métodos não modificam nada, portanto, elas podem ser facilmente protegidas.
22812 Eric Lippert
12

Não existe uma regra rígida e rápida.

Aqui estão algumas regras para tornar o encadeamento de código seguro no .NET e por que essas regras não são boas:

  1. A função e todas as funções que ele chama devem ser puras (sem efeitos colaterais) e usar variáveis ​​locais. Embora isso torne seu código seguro para threads, também há muito pouca coisa interessante que você pode fazer com essa restrição no .NET.
  2. Toda função que opera em um objeto comum deve lock uma coisa comum. Todos os bloqueios devem ser feitos na mesma ordem. Isso tornará o encadeamento de código seguro, mas será incrivelmente lento, e você também não poderá usar vários encadeamentos.
  3. ...

Não existe uma regra que torne o encadeamento de código seguro, a única coisa que você pode fazer é garantir que seu código funcione, não importa quantas vezes ele esteja sendo executado ativamente, cada encadeamento pode ser interrompido a qualquer momento, com cada encadeamento em seu próprio estado / local e isso para cada função (estática ou não) que está acessando objetos comuns.

earlNameless
fonte
10
Os bloqueios não precisam ser lentos. Fechaduras são incrivelmente rápidas; um bloqueio não contestado é da ordem de magnitude de dez a cem nanossegundos . Obviamente, bloqueios contestados são arbitrariamente lentos; se você ficar mais lento porque está contestando bloqueios, refaça a arquitetura do programa para remover a contenção .
Eric Lippert
2
Acho que não consegui esclarecer o número 2. Eu estava tentando dizer que existe uma maneira de tornar os threads seguros: coloque bloqueios em torno de cada acesso a qualquer objeto comum. E supondo que haja vários threads, ele gerará contenção e os bloqueios tornarão a coisa mais lenta. Ao adicionar bloqueios, não se pode apenas adicionar bloqueios às cegas, eles devem ser colocados em lugares estratégicos muito bons.
earlNameless