O thread do construtor estático do C # é seguro?


247

Em outras palavras, esse segmento de implementação Singleton é seguro:

public class Singleton
{
    private static Singleton instance;

    private Singleton() { }

    static Singleton()
    {
        instance = new Singleton();
    }

    public static Singleton Instance
    {
        get { return instance; }
    }
}

1
É seguro para threads. Suponha que vários threads desejem obter a propriedade de Instanceuma só vez. Um dos threads será instruído a executar primeiro o inicializador de tipo (também conhecido como construtor estático). Enquanto isso, todos os outros threads que desejam ler a Instancepropriedade serão bloqueados até que o inicializador de tipo termine. Somente após a conclusão do inicializador de campo, os segmentos poderão obter o Instancevalor. Então ninguém pode ver o Instanceser null.
Jeppe Stig Nielsen

@JeppeStigNielsen Os outros threads não estão bloqueados. Da minha própria experiência, recebi erros desagradáveis ​​por causa disso. A garantia é que apenas o primeiro thread iniciará o inicializador estático, ou o construtor, mas os outros threads tentarão usar um método estático, mesmo que o processo de construção não tenha sido concluído.
Narvalex

2
@Narvalex Este programa de amostra (fonte codificada em URL) não pode reproduzir o problema que você descreve. Talvez dependa de qual versão do CLR você possui?
Jeppe Stig Nielsen

@JeppeStigNielsen Obrigado por dedicar seu tempo. Você pode me explicar por que aqui o campo está vencido?
Narvalex

5
@Narvalex Com esse código, as letras maiúsculas Xacabam sendo iguais, -1 sem o uso de threads . Não é um problema de segurança de threads. Em vez disso, o inicializador x = -1é executado primeiro (ele está em uma linha anterior do código, um número de linha inferior). Em seguida, o inicializador X = GetX()é executado, o que torna maiúscula Xigual a -1. E, em seguida, o construtor estático "explícito", o inicializador de tipo static C() { ... }é executado, que muda apenas em minúsculas x. Então, depois de tudo isso, o Mainmétodo (ou Othermétodo) pode continuar e ler maiúsculas X. Seu valor será -1, mesmo com apenas um segmento.
Jeppe Stig Nielsen

Respostas:


189

É garantido que os construtores estáticos sejam executados apenas uma vez por domínio de aplicativo, antes de qualquer instância de uma classe ser criada ou qualquer membro estático ser acessado. https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/static-constructors

A implementação mostrada é segura para threads para a construção inicial, ou seja, nenhum teste de bloqueio ou nulo é necessário para a construção do objeto Singleton. No entanto, isso não significa que qualquer uso da instância será sincronizado. Existem várias maneiras de fazer isso; Eu mostrei um abaixo.

public class Singleton
{
    private static Singleton instance;
    // Added a static mutex for synchronising use of instance.
    private static System.Threading.Mutex mutex;
    private Singleton() { }
    static Singleton()
    {
        instance = new Singleton();
        mutex = new System.Threading.Mutex();
    }

    public static Singleton Acquire()
    {
        mutex.WaitOne();
        return instance;
    }

    // Each call to Acquire() requires a call to Release()
    public static void Release()
    {
        mutex.ReleaseMutex();
    }
}

53
Observe que, se seu objeto singleton for imutável, o uso de um mutex ou qualquer mecanismo de sincronização é um exagero e não deve ser usado. Além disso, acho a implementação de amostra acima extremamente frágil :-). Espera-se que todo código usando Singleton.Acquire () chame Singleton.Release () quando terminar usando a instância singleton. Caso não faça isso (por exemplo, retornando prematuramente, deixando o escopo por exceção, esquecendo de chamar Release), na próxima vez em que esse Singleton for acessado de um thread diferente, ele entrará em conflito em Singleton.Acquire ().
Milan Gardian

2
Concordo, embora eu vá além. Se o seu singleton é imutável, o uso de um singleton é um exagero. Apenas defina constantes. Por fim, o uso adequado de um singleton exige que os desenvolvedores saibam o que estão fazendo. Por mais frágil que essa implementação seja, ainda é melhor do que a pergunta em que esses erros se manifestam aleatoriamente, e não como um mutex obviamente não lançado.
Zooba

26
Uma maneira de diminuir a fragilidade do método Release () é usar outra classe com o IDisposable como manipulador de sincronização. Quando você adquire o singleton, você obtém o manipulador e pode colocar o código que requer o singleton em um bloco usando para lidar com a liberação.
CodexArcanum

5
Para outras pessoas que podem ser enganadas por isso: Quaisquer membros de campo estático com inicializadores são inicializados antes que o construtor estático seja chamado.
Adam W. McKinley

12
A resposta hoje em dia é usar Lazy<T>- qualquer um que use o código que eu postei originalmente está fazendo errado (e honestamente não era tão bom assim - para 5 anos atrás, eu não era tão bom nessas coisas quanto as atuais -eu é :) ).
Zooba 04/02

86

Embora todas essas respostas dêem a mesma resposta geral, há uma ressalva.

Lembre-se de que todas as derivações potenciais de uma classe genérica são compiladas como tipos individuais. Portanto, tenha cuidado ao implementar construtores estáticos para tipos genéricos.

class MyObject<T>
{
    static MyObject() 
    {
       //this code will get executed for each T.
    }
}

EDITAR:

Aqui está a demonstração:

static void Main(string[] args)
{
    var obj = new Foo<object>();
    var obj2 = new Foo<string>();
}

public class Foo<T>
{
    static Foo()
    {
         System.Diagnostics.Debug.WriteLine(String.Format("Hit {0}", typeof(T).ToString()));        
    }
}

No console:

Hit System.Object
Hit System.String

typeof (MyObject <T>)! = typeof (MyObject <Y>);
precisa

6
Eu acho que é esse o ponto que estou tentando fazer. Os tipos genéricos são compilados como tipos individuais com base nos parâmetros genéricos usados, portanto, o construtor estático pode e será chamado várias vezes.
Brian Rudolph

1
Isso é certo quando T é do tipo valor, para o tipo de referência T, apenas um tipo genérico seria gerado
sll

2
@sll: Not True ... Veja minha edição
Brian Rudolph

2
O cosntructor interessante, mas realmente estático, chamava todos os tipos, apenas tentei para vários tipos de referência
sll

28

Usar um construtor estático é realmente seguro para threads. O construtor estático é garantido para ser executado apenas uma vez.

Na especificação da linguagem C # :

O construtor estático de uma classe é executado no máximo uma vez em um determinado domínio de aplicativo. A execução de um construtor estático é acionada pelo primeiro dos seguintes eventos dentro de um domínio de aplicativo:

  • Uma instância da classe é criada.
  • Qualquer um dos membros estáticos da classe é referenciado.

Então, sim, você pode confiar que seu singleton será instanciado corretamente.

Zooba fez um excelente argumento (e 15 segundos antes de mim também!) De que o construtor estático não garantirá o acesso compartilhado seguro por thread ao singleton. Isso precisará ser tratado de outra maneira.


8

Aqui está a versão Cliffnotes da página acima do MSDN em c # singleton:

Use o seguinte padrão, sempre, você não pode dar errado:

public sealed class Singleton
{
   private static readonly Singleton instance = new Singleton();

   private Singleton(){}

   public static Singleton Instance
   {
      get 
      {
         return instance; 
      }
   }
}

Além dos recursos óbvios do singleton, ele oferece essas duas coisas de graça (em relação ao singleton em c ++):

  1. construção preguiçosa (ou nenhuma construção, se nunca foi chamada)
  2. sincronização

3
Preguiçoso se a classe for não possui outras estatísticas estáticas (como consts). Caso contrário, acessar qualquer método ou propriedade estática resultará na criação da instância. Então eu não chamaria isso de preguiçoso.
Schultz9999

6

É garantido que os construtores estáticos são acionados apenas uma vez por domínio de aplicativo, portanto sua abordagem deve ser boa. No entanto, funcionalmente não é diferente da versão mais concisa e em linha:

private static readonly Singleton instance = new Singleton();

A segurança do encadeamento é mais um problema quando você está inicializando preguiçosamente as coisas.


4
Andrew, isso não é totalmente equivalente. Por não usar um construtor estático, algumas das garantias sobre quando o inicializador será executado são perdidas. Consulte estes links para obter uma explicação detalhada: * < csharpindepth.com/Articles/General/Beforefieldinit.aspx > * < ondotnet.com/pub/a/dotnet/2003/07/07/staticxtor.html >
Derek Park

Derek, eu estou familiarizado com a beforefieldinit "otimização", mas, pessoalmente, eu nunca se preocupar com isso.
Andrew Peters

link de trabalho para o comentário de @ DerekPark: csharpindepth.com/Articles/General/Beforefieldinit.aspx . Este link parece estar obsoleto: ondotnet.com/pub/a/dotnet/2003/07/07/staticxtor.html
phoog

4

O construtor estático terminará a execução antes que qualquer thread possa acessar a classe.

    private class InitializerTest
    {
        static private int _x;
        static public string Status()
        {
            return "_x = " + _x;
        }
        static InitializerTest()
        {
            System.Diagnostics.Debug.WriteLine("InitializerTest() starting.");
            _x = 1;
            Thread.Sleep(3000);
            _x = 2;
            System.Diagnostics.Debug.WriteLine("InitializerTest() finished.");
        }
    }

    private void ClassInitializerInThread()
    {
        System.Diagnostics.Debug.WriteLine(Thread.CurrentThread.GetHashCode() + ": ClassInitializerInThread() starting.");
        string status = InitializerTest.Status();
        System.Diagnostics.Debug.WriteLine(Thread.CurrentThread.GetHashCode() + ": ClassInitializerInThread() status = " + status);
    }

    private void classInitializerButton_Click(object sender, EventArgs e)
    {
        new Thread(ClassInitializerInThread).Start();
        new Thread(ClassInitializerInThread).Start();
        new Thread(ClassInitializerInThread).Start();
    }

O código acima produziu os resultados abaixo.

10: ClassInitializerInThread() starting.
11: ClassInitializerInThread() starting.
12: ClassInitializerInThread() starting.
InitializerTest() starting.
InitializerTest() finished.
11: ClassInitializerInThread() status = _x = 2
The thread 0x2650 has exited with code 0 (0x0).
10: ClassInitializerInThread() status = _x = 2
The thread 0x1f50 has exited with code 0 (0x0).
12: ClassInitializerInThread() status = _x = 2
The thread 0x73c has exited with code 0 (0x0).

Embora o construtor estático tenha demorado muito para ser executado, os outros threads pararam e esperaram. Todos os threads lêem o valor de _x definido na parte inferior do construtor estático.


3

A especificação Common Language Infrastructure garante que "um inicializador de tipo seja executado exatamente uma vez para qualquer tipo, a menos que seja explicitamente chamado pelo código do usuário". (Seção 9.5.3.1.) Portanto, a menos que você tenha alguma IL louca na chamada solta Singleton ::. Cctor diretamente (improvável), seu construtor estático será executado exatamente uma vez antes que o tipo Singleton seja usado, apenas uma instância do Singleton será criada, e sua propriedade Instance é segura para threads.

Observe que, se o construtor de Singleton acessar a propriedade Instance (mesmo que indiretamente), a propriedade Instance será nula. O melhor que você pode fazer é detectar quando isso acontece e lançar uma exceção, verificando se a instância não é nula no acessador de propriedade. Após a conclusão do construtor estático, a propriedade Instance será não nula.

Como a resposta de Zoomba aponta, você precisará tornar o Singleton seguro para acessar a partir de vários threads ou implementar um mecanismo de bloqueio usando a instância singleton.




0

Embora outras respostas estejam na maior parte corretas, há ainda outra ressalva com os construtores estáticos.

De acordo com a seção II.10.5.3.3, corridas e impasses da infraestrutura de idioma comum ECMA-335

A inicialização de tipo sozinha não deve criar um conflito, a menos que algum código chamado de um inicializador de tipo (direta ou indiretamente) invoque explicitamente operações de bloqueio.

O código a seguir resulta em um conflito

using System.Threading;
class MyClass
{
    static void Main() { /* Won’t run... the static constructor deadlocks */  }

    static MyClass()
    {
        Thread thread = new Thread(arg => { });
        thread.Start();
        thread.Join();
    }
}

O autor original é Igor Ostrovsky, veja seu post aqui .

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.