Mike Ash Singleton: MISE EN PLACE @synchronized
-
19-09-2019 - |
Question
Je suis tombé sur ce sur les « soins et l'alimentation des singletons » Mike Ash et a été un peu puzzeled par son commentaire:
Ce code est un peu lent, cependant. Prendre un verrou est un peu cher. Rendre plus douloureux est le fait que la grande majorité du temps, la serrure est inutile. Le verrou est requis uniquement si foo est nul, ce qui essentiellement se produit une seule fois. Après le singleton est initialisé, la nécessité de le verrou est parti, mais la serrure elle-même reste.
+(id)sharedFoo {
static Foo *foo = nil;
@synchronized([Foo class]) {
if(!foo) foo = [[self alloc] init];
}
return foo;
}
Ma question est, et il ne fait aucun doute une bonne raison pour cela, mais pourquoi ne pas pouvez-vous écrire (voir ci-dessous) afin de limiter le verrou quand foo est nul?
+(id)sharedFoo {
static Foo *foo = nil;
if(!foo) {
@synchronized([Foo class]) {
foo = [[self alloc] init];
}
}
return foo;
}
hourras gary
La solution
Parce que le test est soumis à une condition de course. Deux fils différents peuvent tester de façon indépendante que foo
est nil
, puis (séquentiellement) créer des instances distinctes. Cela peut se produire dans votre version modifiée quand un thread effectue le test alors que l'autre est encore à l'intérieur +[Foo alloc]
ou -[Foo init]
, mais n'a pas encore fixé foo
.
Par ailleurs, je ne le ferais pas ça du tout. Consultez la fonction dispatch_once()
, qui vous permet de garantir qu'un bloc ne jamais exécuté une fois au cours (en supposant que vous avez GCD sur la plate-forme que vous ciblez) la durée de vie de votre application.
Autres conseils
Ceci est appelé revérifié verrouillage "optimisation" . Comme indiqué partout ce n'est pas sûr. Même si ce n'est pas vaincu par une optimisation du compilateur, il sera vaincu la mémoire façon dont fonctionne sur les machines modernes, à moins que vous utilisez une sorte de clôture / barrières.
Mike Ash montre également la solution correcte à l'aide volatile
et OSMemoryBarrier();
.
Le problème est que lorsqu'un thread exécute foo = [[self alloc] init];
il n'y a aucune garantie que lorsqu'un autre thread voit foo != 0
toute la mémoire réalisée par init
écritures est visible aussi.
Voir aussi DCL et C ++ et DCL et java pour plus de détails.
Dans votre version du chèque de !foo
pourrait se produire sur plusieurs threads en même temps, ce qui permet deux fils de sauter dans le bloc alloc
, une attente de l'autre pour terminer avant d'allouer une autre instance.
Vous pouvez optimiser en prenant seulement la serrure si foo == nul, mais après que vous avez besoin de tester à nouveau (dans le @synchronized) pour se prémunir contre les conditions de course.
+ (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;
}
La meilleure façon si vous avez grande expédition 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];
}