A continuación se muestra un código de un libro de C# para mostrar cómo se construye el patrón Singleton en subprocesos múltiples:
internal sealed class Singleton { // s_lock is required for thread safety and having this object assumes that creating // the singleton object is more expensive than creating a System.Object object private static readonly Object s_lock = new Object(); // This field will refer to the one Singleton object private static Singleton s_value = null; // Private constructor prevents any code outside this class from creating an instance private Singleton() { // Code to initialize the one Singleton object goes here... } // Public, static method that returns the Singleton object (creating it if necessary) public static Singleton GetSingleton() { // If the Singleton was already created, just return it (this is fast) if (s_value != null) return s_value; Monitor.Enter(s_lock); // Not created, let 1 thread create it if (s_value == null) { // Still not created, create it Singleton temp = new Singleton(); // Save the reference in s_value (see discussion for details) Volatile.Write(ref s_value, temp); } Monitor.Exit(s_lock); // Return a reference to the one Singleton object return s_value; } }
Tengo la idea de por qué el código hace:
Singleton temp = new Singleton(); Volatile.Write(ref s_value, temp);
en vez de
s_value = new Singleton();
porque el compilador puede asignar memoria para Singleton
, asignar la referencia a s_value
y luego llamar al constructor. Desde la perspectiva de un solo subproceso, cambiar el orden de esta manera no tiene ningún impacto. Pero si después de publicar la referencia en s_value
y antes de llamar al constructor, otro subproceso llama al método GetSingleton
, entonces el subproceso verá que s_value
no es nulo y comenzará a usar el objeto Singleton
, pero su constructor aún no ha terminado de ejecutarse.
Pero no entiendo por qué tenemos que usar Volatile.Write
, ¿no podemos hacerlo?
Singleton temp = new Singleton(); s_value = temp;
El compilador no puede reordenar, por ejemplo, ejecutar s_value = temp
primero y luego ejecutar Singleton temp = new Singleton()
, porque temp
tiene que existir antes de s_value = temp
?
Este código es de CLR via C#
por Jeffrey Richter.
La explicación del autor en el libro (como se menciona en el comentario 'ver discusión para obtener detalles' ) es que Volatile.Write
:
asegura que la referencia en
temp
se pueda publicar ens_value
solo después de que el constructor haya terminado de ejecutarse.
Chris Brumme escribió en 2003 sobre un patrón de verificación doble de C# idéntico (los nombres de las variables cambiaron):
Funciona muy bien en X86. Pero se rompería con una implementación legal pero débil de la especificación ECMA CLI.
Suponga que se han realizado una serie de tiendas durante la construcción de [
Singleton
]. Esos almacenes se pueden reordenar arbitrariamente, incluida la posibilidad de retrasarlos hasta que el almacén de publicación asigne el nuevo objeto a [s_value
]. En ese punto, hay una pequeña ventana antes de la tienda. La liberación implica dejar el candado. Dentro de esa ventana, otras CPU pueden navegar a través de la referencia [s_value
] y ver una instancia parcialmente construida.
Por lo tanto, Volatile.Write
solo se requiere cuando se codifica para una implementación (teórica) más débil posible del modelo de memoria estándar ECMA CLI .
NB que el estándar CLI llama a la necesidad de barreras en este caso (12.6.8):
No es un requisito explícito que una implementación conforme de la CLI garantice que todas las actualizaciones de estado realizadas dentro de un constructor sean uniformemente visibles antes de que se complete el constructor. Los generadores de CIL pueden garantizar este requisito ellos mismos insertando llamadas apropiadas a la barrera de memoria o instrucciones de escritura volátiles.
No he podido demostrar prácticamente este problema (visibilidad de una instancia parcialmente construida debido al reordenamiento) con las versiones actuales de .net en x64/ARM64 pero no lo he intentado mucho.
TLDR: escribir este tipo de código es complicado, use Lazy<T>