¿Por qué Apple recomienda usar dispatch_once para implementar el patrón singleton en ARC?

305

¿Cuál es la razón exacta para usar dispatch_once en el acceso de instancia compartida de un singleton en ARC?

+ (MyClass *)sharedInstance
{
    //  Static local predicate must be initialized to 0
    static MyClass *sharedInstance = nil;
    static dispatch_once_t onceToken = 0;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[MyClass alloc] init];
        // Do any other initialisation stuff here
    });
    return sharedInstance;
}

¿No es una mala idea instanciar el singleton asincrónicamente en segundo plano? Quiero decir, ¿qué sucede si solicito esa instancia compartida y confío en ella de inmediato, pero dispatch_once toma hasta Navidad para crear mi objeto? No vuelve de inmediato, ¿verdad? Al menos ese parece ser el objetivo de Grand Central Dispatch.

Entonces, ¿por qué están haciendo esto?

Miembro orgulloso
fuente
Note: static and global variables default to zero.
ikkentim

Respuestas:

418

dispatch_once()Es absolutamente sincrónico. No todos los métodos GCD hacen las cosas de forma asíncrona (en este caso, dispatch_sync()es síncrono). El uso de dispatch_once()reemplaza el siguiente modismo:

+ (MyClass *)sharedInstance {
    static MyClass *sharedInstance;
    @synchronized(self) {
        if (sharedInstance == nil) {
            sharedInstance = [[MyClass alloc] init];
        }
    }
    return sharedInstance;
}

El beneficio de dispatch_once()esto es que es más rápido. También es semánticamente más limpio, porque también lo protege de múltiples subprocesos haciendo alloc init de su SharedInstance, si todos lo intentan al mismo tiempo exacto. No permitirá que se creen dos instancias. La idea dispatch_once()es "realizar algo una vez y solo una vez", que es precisamente lo que estamos haciendo.

Lily Ballard
fuente
44
Por el bien del argumento, debo tener en cuenta que la documentación no dice que se está ejecutando sincrónicamente. Solo dice que se invocarán múltiples invocaciones simultáneas.
experto
55
Afirmas que es más rápido, ¿cuánto más rápido? No tengo ninguna razón para pensar que no estás diciendo la verdad, pero me gustaría ver un punto de referencia simple.
Joshua Gross el
29
Acabo de hacer un punto de referencia simple (en iPhone 5) y parece que dispatch_once es aproximadamente 2 veces más rápido que @synchronized.
Joshua Gross el
3
@ReneDohan: Si está 100% seguro de que nadie llama a ese método desde un hilo diferente, entonces funciona. Pero el uso dispatch_once()es realmente simple (especialmente porque Xcode incluso lo completará automáticamente en un fragmento de código completo para usted) y significa que nunca tendrá que considerar si el método debe ser seguro para subprocesos.
Lily Ballard
1
@siuying: En realidad, eso no es cierto. Primero, todo lo que se hace +initializeantes de que se toque la clase, incluso si aún no está intentando crear su instancia compartida. En general, la inicialización diferida (crear algo solo cuando es necesario) es mejor. En segundo lugar, incluso su reclamo de rendimiento no es cierto. dispatch_once()tiene casi exactamente la misma cantidad de tiempo como decir if (self == [MyClass class])en +initialize. Si ya tiene un +initialize, entonces sí, crear la instancia compartida allí es más rápido, pero la mayoría de las clases no.
Lily Ballard
41

Porque solo se ejecutará una vez. Por lo tanto, si intenta acceder a él dos veces desde diferentes hilos, no causará ningún problema.

Mike Ash tiene una descripción completa en su publicación de blog Care and Feeding of Singletons .

No todos los bloques GCD se ejecutan de forma asincrónica.

Abizern
fuente
La respuesta de Lily es mejor, pero voy a dejar la mía para mantener el enlace a la publicación de Mike Ash.
Abizern