Mike Ash Singleton: colocar @synchronized
-
19-09-2019 - |
Pergunta
I veio em este sobre a Mike Ash "cuidados e alimentação de singletons" e foi um pouco puzzeled pelo seu comentário:
Este código é uma espécie de lento, no entanto. Tomando um bloqueio é um pouco caro. Tornando-o mais doloroso é o fato que a grande maioria das vezes, o bloqueio é inútil. O bloqueio é só precisava quando foo é nulo, ou seja basicamente só acontece uma vez. Depois de Singleton é inicializado, a necessidade de o bloqueio se foi, mas o próprio bloqueio restos.
+(id)sharedFoo {
static Foo *foo = nil;
@synchronized([Foo class]) {
if(!foo) foo = [[self alloc] init];
}
return foo;
}
A minha pergunta é, e não há dúvida uma boa razão para isso, mas por que você não pode escrever (veja abaixo) para limitar o bloqueio para quando foo é nulo?
+(id)sharedFoo {
static Foo *foo = nil;
if(!foo) {
@synchronized([Foo class]) {
foo = [[self alloc] init];
}
}
return foo;
}
aplausos Gary
Solução
Porque então o teste está sujeita a uma condição de corrida. Dois segmentos diferentes podem testar independentemente foo
que é nil
, e, em seguida, (sequencialmente) criar instâncias separadas. Isso pode acontecer em sua versão modificada quando um executa rosca o teste, enquanto o outro ainda está dentro +[Foo alloc]
ou -[Foo init]
, mas ainda não tem conjunto foo
.
A propósito, eu não iria fazê-lo dessa maneira em tudo. Confira a função dispatch_once()
, que permite garantir que um bloco é sempre apenas executado uma vez durante a vida de seu aplicativo (supondo que você tem GCD na plataforma que você está alvejando).
Outras dicas
Este é o chamado dobro verificado bloqueio "otimização" . Conforme documentado em todos os lugares isso não é seguro. Mesmo se não é derrotado por uma otimização do compilador, ele será derrotado as obras de memória forma em máquinas modernas, a menos que você use algum tipo de cercas / barreiras.
Mike Ash mostra também a solução correta usando volatile
e OSMemoryBarrier();
.
A questão é que, quando se executa rosca foo = [[self alloc] init];
não há garantia de que, quando um outro segmento vê foo != 0
toda a memória escreve realizada por init
é visível também.
Veja também DCL e C ++ e DCL e java para mais detalhes.
Na sua versão do cheque de !foo
poderia ser ocorrendo em vários threads ao mesmo tempo, permitindo que dois threads para saltar para o bloco alloc
, um espera que o outro terminar antes de alocar outra instância.
Você pode otimizar tomando somente o bloqueio se foo nil ==, mas depois que você precisa para testar novamente (dentro do @synchronized) para se proteger contra as condições de corrida.
+ (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;
}
A melhor maneira se você tiver expedição Grand Central
+ (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];
}