¿Es seguro el hilo del constructor estático C #?

247

En otras palabras, ¿es seguro este hilo de implementación de Singleton?

public class Singleton
{
    private static Singleton instance;

    private Singleton() { }

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

    public static Singleton Instance
    {
        get { return instance; }
    }
}
urini
fuente
1
Es seguro para subprocesos. Supongamos que varios hilos quieren obtener la propiedad Instancea la vez. Se le indicará a uno de los hilos que primero ejecute el inicializador de tipo (también conocido como el constructor estático). Mientras tanto, todos los otros hilos que quieran leer la Instancepropiedad se bloquearán hasta que el inicializador de tipo haya finalizado. Solo después de que el inicializador de campo haya concluido, se permitirá que los hilos obtengan el Instancevalor. Entonces nadie puede ver el Instanceser null.
Jeppe Stig Nielsen
@JeppeStigNielsen Los otros hilos no están bloqueados. Por mi propia experiencia, tuve errores desagradables debido a eso. La garantía es que solo el primer subproceso iniciará el inicializador estático o el constructor, pero luego los otros subprocesos intentarán usar un método estático, incluso si el proceso de construcción no terminó.
Narvalex
2
@Narvalex Este programa de muestra (fuente codificada en URL) no puede reproducir el problema que describe. ¿Tal vez depende de qué versión de CLR tienes?
Jeppe Stig Nielsen
@JeppeStigNielsen Gracias por tomarse su tiempo. ¿Puede explicarme por qué aquí se anula el campo?
Narvalex
55
@Narvalex Con ese código, las mayúsculas Xterminan siendo -1 incluso sin subprocesos . No es un problema de seguridad de hilos. En cambio, el inicializador se x = -1ejecuta primero (está en una línea anterior en el código, un número de línea inferior). Luego se X = GetX()ejecuta el inicializador , lo que hace que las mayúsculas sean Xiguales a -1. Y luego el constructor estático "explícito", se static C() { ... }ejecuta el inicializador de tipo , que cambia solo en minúsculas x. Entonces, después de todo eso, el Mainmétodo (o Othermétodo) puede continuar y leer mayúsculas X. Su valor será -1, incluso con un solo hilo.
Jeppe Stig Nielsen

Respuestas:

189

Se garantiza que los constructores estáticos se ejecuten solo una vez por dominio de aplicación, antes de que se creen instancias de una clase o se acceda a cualquier miembro estático. https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/static-constructors

La implementación que se muestra es segura para subprocesos para la construcción inicial, es decir, no se requiere ninguna prueba de bloqueo o nula para construir el objeto Singleton. Sin embargo, esto no significa que cualquier uso de la instancia se sincronizará. Hay una variedad de formas en que esto se puede hacer; He mostrado uno a continuación.

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
fuente
53
Tenga en cuenta que si su objeto singleton es inmutable, usar un mutex o cualquier mecanismo de sincronización es una exageración y no debe usarse. Además, la implementación de muestra anterior me parece extremadamente frágil :-). Se espera que todo el código que usa Singleton.Acquire () llame a Singleton.Release () cuando se hace usando la instancia de singleton. Si no se hace esto (por ejemplo, regresar prematuramente, dejando el alcance por excepción, olvidando llamar a Release), la próxima vez que se acceda a este Singleton desde un hilo diferente, se bloqueará en Singleton.Acquire ().
Milan Gardian
2
De acuerdo, aunque iría más lejos. Si su singleton es inmutable, usar un singleton es excesivo. Solo define constantes. Finalmente, usar un singleton correctamente requiere que los desarrolladores sepan lo que están haciendo. Tan frágil como es esta implementación, aún es mejor que la de la pregunta en la que esos errores se manifiestan al azar en lugar de ser un mutex obviamente inédito.
Zooba
26
Una forma de disminuir la fragilidad del método Release () es utilizar otra clase con IDisposable como controlador de sincronización. Cuando adquiere el singleton, obtiene el controlador y puede colocar el código que requiere el singleton en un bloque de uso para manejar la liberación.
CodexArcanum
55
Para otros que podrían verse afectados por esto: cualquier miembro de campo estático con inicializadores se inicializa antes de que se llame al constructor estático.
Adam W. McKinley
12
La respuesta en estos días es usar Lazy<T>: cualquiera que use el código que publiqué originalmente lo está haciendo mal (y, sinceramente, no era tan bueno para empezar, hace 5 años, yo no era tan bueno en estas cosas como en la actualidad) -me es :)).
Zooba
86

Si bien todas estas respuestas dan la misma respuesta general, hay una advertencia.

Recuerde que todas las derivaciones potenciales de una clase genérica se compilan como tipos individuales. Por lo tanto, tenga cuidado al implementar constructores estáticos para tipos genéricos.

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

EDITAR:

Aquí está la demostración:

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()));        
    }
}

En la consola:

Hit System.Object
Hit System.String
Brian Rudolph
fuente
typeof (MyObject <T>)! = typeof (MyObject <Y>);
Karim Agha
66
Creo que ese es el punto que estoy tratando de hacer. Los tipos genéricos se compilan como tipos individuales en función de los parámetros genéricos que se utilizan, por lo que el constructor estático puede y será llamado varias veces.
Brian Rudolph el
1
Esto es correcto cuando T es de tipo de valor, para el tipo de referencia T solo se generaría un tipo genérico
todo el
2
@sll: No es cierto ... Vea mi edición
Brian Rudolph
2
El constructor interesante pero realmente estático requirió todos los tipos, solo probé para múltiples tipos de referencia
todo el
28

Usar un constructor estático en realidad es seguro para subprocesos. El constructor estático está garantizado para ejecutarse solo una vez.

De la especificación del lenguaje C # :

El constructor estático para una clase se ejecuta como máximo una vez en un dominio de aplicación dado. La ejecución de un constructor estático se desencadena por el primero de los siguientes eventos que ocurren dentro del dominio de una aplicación:

  • Se crea una instancia de la clase.
  • Se hace referencia a cualquiera de los miembros estáticos de la clase.

Entonces, sí, puede confiar en que su singleton se instanciará correctamente.

Zooba hizo un excelente comentario (¡y 15 segundos antes que yo también!) De que el constructor estático no garantizará un acceso compartido seguro para subprocesos al singleton. Eso tendrá que ser manejado de otra manera.

Derek Park
fuente
8

Aquí está la versión Cliffnotes de la página MSDN anterior en c # singleton:

Utilice el siguiente patrón, siempre, no puede salir mal:

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

   private Singleton(){}

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

Más allá de las características obvias de singleton, le ofrece estas dos cosas de forma gratuita (con respecto a singleton en c ++):

  1. construcción perezosa (o ninguna construcción si nunca se llamó)
  2. sincronización
Jay Juch
fuente
3
Perezoso si la clase no tiene ninguna otra estadística no relacionada (como consts). De lo contrario, acceder a cualquier método o propiedad estática dará como resultado la creación de la instancia. Entonces no lo llamaría perezoso.
Schultz9999
6

Se garantiza que los constructores estáticos se dispararán solo una vez por dominio de aplicación, por lo que su enfoque debería ser correcto. Sin embargo, funcionalmente no es diferente de la versión en línea más concisa:

private static readonly Singleton instance = new Singleton();

La seguridad de subprocesos es más un problema cuando está inicializando cosas perezosamente.

Andrew Peters
fuente
44
Andrew, eso no es totalmente equivalente. Al no utilizar un constructor estático, se pierden algunas de las garantías sobre cuándo se ejecutará el inicializador. Consulte estos enlaces para obtener una explicación detallada: * < csharpindepth.com/Articles/General/Beforefieldinit.aspx > * < ondotnet.com/pub/a/dotnet/2003/07/07/staticxtor.html >
Derek Park
Derek, estoy familiarizado con el beforefieldinit "optimización", pero, personalmente, nunca me preocupo por ella.
Andrew Peters
enlace de trabajo para el comentario de @ DerekPark: csharpindepth.com/Articles/General/Beforefieldinit.aspx . Este enlace parece estar obsoleto: ondotnet.com/pub/a/dotnet/2003/07/07/staticxtor.html
phoog
4

El constructor estático terminará de ejecutarse antes de que cualquier hilo pueda acceder a la clase.

    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();
    }

El código anterior produjo los resultados a continuación.

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).

Aunque el constructor estático tardó mucho en ejecutarse, los otros subprocesos se detuvieron y esperaron. Todos los hilos leen el valor de _x establecido en la parte inferior del constructor estático.

Ideas comerciales Philip
fuente
3

La especificación de Common Language Infrastructure garantiza que "un inicializador de tipo se ejecutará exactamente una vez para cualquier tipo dado, a menos que el código de usuario lo llame explícitamente". (Sección 9.5.3.1.) Entonces, a menos que tenga un poco de IL en la llamada suelta Singleton ::. Cctor directamente (poco probable) su constructor estático se ejecutará exactamente una vez antes de que se use el tipo Singleton, solo se creará una instancia de Singleton, y su propiedad de instancia es segura para subprocesos.

Tenga en cuenta que si el constructor de Singleton accede a la propiedad Instancia (incluso indirectamente), la propiedad Instancia será nula. Lo mejor que puede hacer es detectar cuándo sucede esto y lanzar una excepción, verificando que esa instancia no sea nula en el descriptor de acceso a la propiedad. Después de que su constructor estático se complete, la propiedad Instancia no será nula.

Como señala la respuesta de Zoomba, deberá hacer que Singleton sea seguro para acceder desde múltiples subprocesos, o implementar un mecanismo de bloqueo utilizando la instancia de singleton.

Dominic Cooney
fuente
0

Aunque otras respuestas son en su mayoría correctas, hay otra advertencia con los constructores estáticos.

Según la sección II.10.5.3.3 Razas y puntos muertos de la infraestructura de lenguaje común ECMA-335

La inicialización de tipo por sí sola no creará un punto muerto a menos que algún código invocado desde un inicializador de tipo (directa o indirectamente) invoque explícitamente operaciones de bloqueo.

El siguiente código da como resultado un punto muerto

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();
    }
}

El autor original es Igor Ostrovsky, vea su publicación aquí .

oleksii
fuente