2010-03-10 12 views
7

me encontré con esto en el Mike Ash "Cuidado y alimentación de los hijos únicos" y estaba un poco Puzzeled por su comentario:Mike Ash Singleton: Colocación @synchronized

Este código es un poco lento, sin embargo. Tener un candado es algo costoso. Haciéndolo más doloroso es el hecho de que la gran mayoría de las veces, el bloqueo no tiene sentido. El bloqueo es solo necesario cuando foo es nulo, lo que básicamente solo ocurre una vez. Después de que se inicializa el singleton , la necesidad de se ha ido, pero la cerradura en sí permanece.

+(id)sharedFoo { 
    static Foo *foo = nil; 
    @synchronized([Foo class]) { 
     if(!foo) foo = [[self alloc] init]; 
    } 
    return foo; 
} 

Mi pregunta es, y no hay duda de una buena razón para esto, pero ¿por qué no se puede escribir (véase más adelante) para limitar la cerradura para cuando foo es nulo?

+(id)sharedFoo { 
    static Foo *foo = nil; 
    if(!foo) { 
     @synchronized([Foo class]) { 
      foo = [[self alloc] init]; 
     } 
    } 
    return foo; 
} 

aplausos gary

+1

Ah bien, así que básicamente necesita un chequeo dentro del bloque @synchronize? – fuzzygoat

+0

Ese es el objetivo de @synchronized: permitir un hilo a la vez para realizar la comprobación. –

+0

Pruebe dispatch_once() en su lugar: http://stackoverflow.com/q/5720029/290295 – ctpenrose

Respuesta

18

Porque entonces la prueba está sujeta a una condición de carrera. Dos subprocesos diferentes pueden probar independientemente que foo es nil y luego (secuencialmente) crear instancias separadas. Esto puede suceder en su versión modificada cuando un hilo realiza la prueba mientras que el otro todavía está dentro de +[Foo alloc] o -[Foo init], pero aún no ha establecido foo.

Por cierto, no lo haría de esa manera en absoluto. Echa un vistazo a la función dispatch_once(), que te permite garantizar que un bloque solo se ejecute una vez durante la vida de tu aplicación (suponiendo que tienes GCD en la plataforma a la que te diriges).

+0

Eso es por supuesto cierto. Pero la mejor solución no sería probar dos veces (dentro de ** y ** fuera del '@ synchronized'). Entonces no habría condición de carrera ni penalización de rendimiento. –

+1

@Nikolai: dime que hay una penalización en el rendimiento _después_ de que hayas ejecutado Shark. :-) –

+0

@Graham: no hay duda de que el rendimiento es malo en la versión original que siempre lleva el bloqueo. Lo tenía en mi código * y lo hice ejecutar Shark *;). Además, Mike Ash lo señaló en su publicación original en el blog. –

1

En su versión, la comprobación de !foo podría estar ocurriendo en múltiples hilos al mismo tiempo, permitiendo que dos hilos salten al bloque alloc, esperando que el otro termine antes de asignar otra instancia.

1

Puede optimizar tomando solo el bloqueo si foo == nil, pero luego debe volver a realizar la prueba (dentro de @synchronized) para protegerse de las condiciones de carrera.

+ (id)sharedFoo { 
    static Foo *foo = nil; 
    if(!foo) { 
     @synchronized([Foo class]) { 
      if (!foo) // test again, in case 2 threads doing this at once 
       foo = [[self alloc] init]; 
     } 
    } 
    return foo; 
} 
+2

Consulte la respuesta de @mfazekas para saber por qué esto es incorrecto. –

7

Esto se llama double checked locking "optimization". Como está documentado en todas partes esto no es seguro. Incluso si no es derrotado por una optimización del compilador, será derrotado como funciona la memoria en las máquinas modernas, a menos que use algún tipo de valla/barreras.

Mike Ash also shows la solución correcta usando volatile y OSMemoryBarrier();.

El problema es que cuando se ejecuta un hilo foo = [[self alloc] init]; no hay garantía de que cuando un otro hilo ve foo != 0 todas las escrituras en memoria realizada por init es visible también.

También vea DCL and C++ y DCL and java para más detalles.

+0

+1 Gracias por dejar esto en claro. El reordenamiento de la instrucción y el acceso a la memoria fuera de orden son conceptos de los que la mayoría de los programadores no son conscientes. –

+3

dispatch_once es la verdadera solución, solo usa eso y deja de hackear – slf

+0

Creo que slf lo tiene. http://stackoverflow.com/q/5720029/290295 – ctpenrose

1

mejor manera si usted tiene gran expedición cenral

+ (MySingleton*) instance { 
static dispatch_once_t _singletonPredicate; 
static MySingleton *_singleton = nil; 

dispatch_once(&_singletonPredicate, ^{ 
    _singleton = [[super allocWithZone:nil] init]; 
}); 

return _singleton 
} 
+ (id) allocWithZone:(NSZone *)zone { 
    return [self instance]; 
}