Mike Ash Singleton: Effettuare @synchronized
-
19-09-2019 - |
Domanda
Sono venuto incontrato questo sulla "cura e l'alimentazione dei single" Mike Ash ed era un po Puzzeled dal suo commento:
Questo codice è un po 'lento, però. Prendendo un blocco è un po 'costoso. Il che rende più doloroso è il fatto che la stragrande maggioranza del tempo, il blocco è inutile. La serratura è necessaria solo quando foo è trascurabili, fondamentalmente accade solo una volta. Dopo il Singleton viene inizializzato, la necessità di il blocco è andato, ma la serratura stessa rimane.
+(id)sharedFoo {
static Foo *foo = nil;
@synchronized([Foo class]) {
if(!foo) foo = [[self alloc] init];
}
return foo;
}
La mia domanda è, e non v'è alcun dubbio una buona ragione per questo, ma perché non si può scrivere (vedi sotto) per limitare la serratura quando foo è pari a zero?
+(id)sharedFoo {
static Foo *foo = nil;
if(!foo) {
@synchronized([Foo class]) {
foo = [[self alloc] init];
}
}
return foo;
}
applausi Gary
Soluzione
Perché allora il test è soggetto a una condizione di competizione. Due fili diversi potrebbero testare autonomamente che foo
è nil
, e poi (sequenziale) creare istanze separate. Questo può accadere nella vostra versione modificata quando un thread esegue il test, mentre l'altro è ancora all'interno +[Foo alloc]
o -[Foo init]
, ma non ha ancora fissato foo
.
A proposito, io non lo farei in questo modo a tutti. Controlla la funzione dispatch_once()
, che consente di garantire che un blocco è sempre e solo eseguito una volta durante la vita di tua app (ammesso che abbiate GCD sulla piattaforma che stai targeting).
Altri suggerimenti
Questa è chiamata la doppia checked bloccaggio "ottimizzazione" . Come documentato in tutto il mondo questo non è sicuro. Anche se non è sconfitto da un ottimizzazione del compilatore, che sarà sconfitto la memoria modo in cui funziona su macchine moderne, a meno che non si utilizza un qualche tipo di recinzione / barriere.
Mike Ash mostra anche la soluzione corretta usando volatile
e OSMemoryBarrier();
.
Il problema è che quando un thread esegue foo = [[self alloc] init];
v'è alcuna garanzia che quando un altro thread vede foo != 0
tutta la memoria scrive eseguita da init
è visibile anche.
DCL e C ++ e DCL e java per ulteriori dettagli.
Nella versione del controllo per !foo
potrebbe avvenire su più thread contemporaneamente, consentendo a due fili di saltare nel blocco alloc
, uno in attesa che l'altro traguardo prima ripartizione un'altra istanza.
È possibile ottimizzare esclusivamente la serratura se foo == pari a zero, ma dopo che è necessario testare ancora una volta (all'interno della @synchronized) per premunirsi contro condizioni di gara.
+ (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;
}
Il modo migliore se si dispone di grande spedizione centrale
+ (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];
}