Mike Ash Singleton: размещение @synchronized

Я приехал через это в Mike Ash "Уход и питание одиночных элементов" и был небольшим puzzeled его комментарием:

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

+(id)sharedFoo {
    static Foo *foo = nil;
    @synchronized([Foo class]) {
        if(!foo) foo = [[self alloc] init];
    }
    return foo;
}

Мой вопрос, и нет сомнения, что серьезное основание для этого, но почему Вы не можете записать (см. ниже) ограничить блокировку тем, когда нечто является нолем?

+(id)sharedFoo {
    static Foo *foo = nil;
    if(!foo) {
        @synchronized([Foo class]) {
            foo = [[self alloc] init];
        }
    }
    return foo;
}

аплодисменты gary

7
задан GingerPlusPlus 13 February 2016 в 16:44
поделиться

4 ответа

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

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

18
ответ дан 6 December 2019 в 06:24
поделиться

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

1
ответ дан 6 December 2019 в 06:24
поделиться

Вы можете оптимизировать, только взяв блокировку, если 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
ответ дан 6 December 2019 в 06:24
поделиться

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

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

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

Также см. DCL и C ++ и DCL и java для получения дополнительных сведений.

7
ответ дан 6 December 2019 в 06:24
поделиться