Поточно-безопасная ленивая инициализация в геттере

Я хотел бы знать, верны ли оба следующих решения для ленивой инициализации.

У меня есть класс AppContext, который должен содержать ссылки на другой класс, который должен существовать только один раз (не делая каждый из этих классов одноэлементным). Допустим, один из этих других классов называется ReferencedClass. При этом я хотел бы лениво инициализировать ссылки со значениями по умолчанию потокобезопасным способом.

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


Решение 1. Изначально я хотел реализовать это следующим образом:

// Getter with lazy initialized default value
- (ReferencedClass *)referencedClass {
    // Check if nil. If yes, wait for lock and check again after locking.
    if (_referencedClass == nil) { 
        @synchronized(self) {
            if (_referencedClass == nil) { 
                // Prevent _referencedClass pointing to partially initialized objects
                ReferencedClass *temp = [[ReferencedClass alloc] init]; 
                _referencedClass = temp;
            }
        }
    }
    return _referencedClass;
}

// Setter
- (void)setReferencedClass:(ReferencedClass *)referencedClass {
    @synchronized(self) {
        _referencedClass = referencedClass;
    }
}

Решение 2. Затем я решил использовать GCD, поэтому написал следующее:

// Getter with lazy initialized default value
- (ReferencedClass *)referencedClass {
    // Check if nil. If yes, wait for "lock" and check again after "locking".
    if (_referencedClass == nil) { 
        dispatch_sync(syncDispatchQueue, ^{
            if (_referencedClass == nil) {
                // Prevent _referencedClass pointing to partially initialized objects
                ReferencedClass *temp = [[ReferencedClass alloc] init]; 
                _referencedClass = temp;
            }
        });
    }
    return _referencedClass;
}

// Setter
- (void)setReferencedClass:(ReferencedClass *)referencedClass {
    dispatch_sync(syncDispatchQueue, ^{
        _referencedClass = referencedClass;
    });
}

Конечно, где-то (например, в методе init) я инициализировал syncDispatchQueue чем-то вроде:

syncDispatchQueue = dispatch_queue_create("com.stackoverflow.lazy", NULL);

Является ли это правильным, потокобезопасным и свободным от взаимоблокировок кодом? Могу ли я использовать блокировку с двойной проверкой вместе с переменной temp? Если эта блокировка с двойной проверкой небезопасна, будет ли мой код в обоих случаях безопасным, если я удалю внешние проверки? Я так думаю, да?

Большое спасибо заранее!

[Примечание: мне известно о dispatch_once и о том, что некоторые говорят, что (вопреки документации Apple) его также можно использовать с переменными экземпляра. Пока я хотел бы использовать один из этих двух вариантов. Если возможно. ]


person michaelk    schedule 17.09.2013    source источник


Ответы (1)


Насколько я понимаю, ваш механизм "блокировки с двойной проверкой" не потокобезопасен, потому что присваивание _referencedClass = ... не является атомарным. Таким образом, один поток может прочитать частично инициализированную переменную во внешней проверке if (_referencedClass == nil).

Если вы удалите внешние проверки, обе версии выглядят нормально.

Вас может заинтересовать

у которого есть отличный ответ, объясняющий различия в реализации и производительности.

person Martin R    schedule 17.09.2013
comment
Спасибо Мартин за быстрый ответ. Я обновил приведенный выше код, чтобы избежать упомянутой вами проблемы. Будет ли это работать сейчас? В противном случае, я думаю, я пропущу внешние проверки, все равно не повредит. Спасибо за ссылку, действительно интересно. - person michaelk; 17.09.2013
comment
@LKK: _referencedClass = temp; не решает проблему, потому что это назначение также не является атомарным. Вы должны удалить внешний чек. - person Martin R; 17.09.2013