Майк Эш Синглтон:Размещение @synchronized
-
19-09-2019 - |
Вопрос
Я наткнулся на это в статье Майка Эша «Уход и кормление одиночек» и был немного озадачен его комментарием:
Однако этот код довольно медленный.Взять замок довольно дорого.Еще более болезненным является тот факт, что что в подавляющем большинстве случаев Замок не имеет смысла.Замок Нужен только тогда, когда foo равен nil, что В основном это происходит только один раз.После того, как singleton, необходимость в Замок пропал, но сам замок остатки.
+(id)sharedFoo {
static Foo *foo = nil;
@synchronized([Foo class]) {
if(!foo) foo = [[self alloc] init];
}
return foo;
}
У меня вопрос, и для этого, несомненно, есть веская причина, но почему вы не можете написать (см. ниже), чтобы ограничить блокировку, когда foo равно нулю?
+(id)sharedFoo {
static Foo *foo = nil;
if(!foo) {
@synchronized([Foo class]) {
foo = [[self alloc] init];
}
}
return foo;
}
ура, Гэри
Решение
Потому что тогда тест будет зависеть от состояния гонки.Два разных потока могут независимо проверить это foo
является nil
, а затем (последовательно) создайте отдельные экземпляры.Это может произойти в вашей модифицированной версии, когда один поток выполняет тест, а другой все еще находится внутри. +[Foo alloc]
или -[Foo init]
, но еще не поставил foo
.
Кстати, я бы так вообще не делал.Проверьте dispatch_once()
функция, которая позволяет вам гарантировать, что блок выполняется только один раз за время существования вашего приложения (при условии, что на целевой платформе у вас есть GCD).
Другие советы
Это называется дважды проверенная блокировка «оптимизация».Как везде документировано, это небезопасно.Даже если его не победить с помощью оптимизации компилятора, он будет побежден так же, как память работает на современных машинах, если только вы не используете какие-то ограждения/барьеры.
Майк Эш также показывает правильное решение с помощью volatile
и OSMemoryBarrier();
.
Проблема в том, что когда один поток выполняется foo = [[self alloc] init];
нет гарантии, что когда другой поток увидит foo != 0
все записи в память, выполняемые init
тоже видно.
Также см DCL и С++ и DCL и Java Больше подробностей.
В вашей версии проверка на !foo
может происходить в нескольких потоках одновременно, что позволяет двум потокам перейти в alloc
блок, один из которых ждет завершения другого, прежде чем выделить другой экземпляр.
Вы можете оптимизировать, взяв блокировку только в том случае, если foo==nil, но после этого вам нужно снова протестировать (в рамках @synchronized), чтобы защититься от условий гонки.
+ (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;
}
Лучший способ, если у вас есть центральная диспетчерская служба.
+ (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];
}