Майк Эш Синглтон: Размещение @synchronized - PullRequest
7 голосов
/ 10 марта 2010

Я наткнулся на это на Майке Эше «Уход и кормление одиночек» и был немного озадачен его комментарием:

Этот код довольно медленный. Взять замок довольно дорого. Делать это более болезненным является фактом что подавляющее большинство времени, замок бессмыслен. Замок есть нужен только когда foo равен нулю, что в основном происходит только один раз. После инициализируется синглтон, необходимость замок исчез, но сам замок остается.

+(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;
}

ура Гэри

Ответы [ 5 ]

18 голосов
/ 10 марта 2010

Потому что тогда испытание зависит от состояния гонки. Два разных потока могут независимо проверять, что foo равно nil, а затем (последовательно) создавать отдельные экземпляры. Это может произойти в вашей модифицированной версии, когда один поток выполняет тест, в то время как другой все еще находится внутри +[Foo alloc] или -[Foo init], но еще не установил foo.

Кстати, я бы так не поступил. Проверьте функцию dispatch_once(), которая позволяет вам гарантировать, что блок выполняется только один раз за время существования вашего приложения (при условии, что у вас есть GCD на целевой платформе).

7 голосов
/ 11 марта 2010

Это называется двойной проверкой блокировки "оптимизация" . Как указано везде, это небезопасно. Даже если он не будет побежден оптимизацией компилятора, он будет побежден тем, как работает память на современных машинах, если вы не используете какие-то ограждения / барьеры.

Майк Эш также показывает правильное решение, используя volatile и OSMemoryBarrier();.

Проблема в том, что когда один поток выполняет foo = [[self alloc] init];, нет гарантии, что когда другой поток увидит foo != 0, все операции записи в память, выполненные init, также будут видны.

Также см. DCL и C ++ и DCL и java для получения более подробной информации.

1 голос
/ 07 марта 2014

Лучший способ, если у вас грандиозная центральная отправка

+ (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];
 }
1 голос
/ 10 марта 2010

Вы можете оптимизировать, взяв блокировку только если 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;
}
1 голос
/ 10 марта 2010

В вашей версии проверка на !foo может происходить в нескольких потоках одновременно, что позволяет двум потокам переходить в блок alloc, один из которых ожидает завершения другого, прежде чем выделить другой экземпляр.

...