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; }
    }
}
urini
fonte
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();
    }
}
Zooba
fonte
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
Brian Rudolph
fonte
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.

Derek Park
fonte
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
Jay Juch
fonte
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.

Andrew Peters
fonte
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.

Philip Trade-Ideas
fonte
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.

Dominic Cooney
fonte
2

Só para ser pedante, mas não existe construtor estático, mas inicializadores de tipo estático, aqui está uma pequena demonstração da dependência cíclica do construtor estático que ilustra esse ponto.

Florian Doyon
fonte
1
A Microsoft parece discordar. msdn.microsoft.com/pt-br/library/k9x6w0hc.aspx
Westy92
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 .

oleksii
fonte