Есть ли потенциальные недостатки этой реализации Singleton для iOS? - PullRequest
1 голос
/ 27 февраля 2012

Самая прямая и простая реализация хотела бы это

static MySingleton *_instance = nil;

    + (MySingleton *) instance
{
    @synchronized (_instance)
    {
        if (_instance == nil)
        {
            _instance = [[MySingleton alloc] init];
        }

        return _instance;
    }
}

На самом деле я знал несколько популярных постов о синглтоне, таких как Реализация Singleton в iOS и шаблон pop

Так что мой вопрос здесь будет "какие-либо дефекты вышеуказанной реализации"?

Ответы [ 2 ]

5 голосов
/ 27 февраля 2012

Да, в вашей реализации есть большой недостаток.Директива @synchronized превращается в вызов objc_sync_enter и последующий вызов objc_sync_exit.Когда вы вызываете метод instance в первый раз, _instance равен nil.Функция objc_sync_enter не блокирует, если вы передадите ей nil, как вы можете видеть по , глядя на ее исходный код .

Так что, если два потока одновременно вызывают instance перед _instance был инициализирован, вы создадите два экземпляра MySingleton.

Кроме того, вы должны поместить переменную _instance в функцию, если у вас нет причин выставлять ее всему исходному файлу.

Предпочтительная реализация одноэлементного средства доступа для iOS 4.0 и более поздних версий использует очень эффективную функцию dispatch_once и выглядит следующим образом:

+ (MySingleton *)sharedInstance {
    static MySingleton *theInstance;
    static dispatch_once_t once;
    dispatch_once(&once, ^{
        theInstance = [[self alloc] init];
    });
    return theInstance;
}

Функция dispatch_once недоступна до iOS4.0, так что если вам действительно нужно поддерживать более старые версии iOS (что маловероятно), вы должны использовать менее эффективный @synchronized.Так как вы не можете синхронизироваться на nil, вы синхронизируете на объекте класса:

+ (MySingleton *)sharedInstance {
    static volatile MySingleton *theInstance;
    if (!theInstance) {
        @synchronized (self) {
            if (!theInstance)
                theInstance = [[self alloc] init];
        }
    }
    return theInstance;
}
1 голос
/ 27 февраля 2012

Как должен выглядеть мой синглтон Objective-C? - хорошая дискуссия для одиноких (как указал Джош), но для ответа на ваш вопрос:

Нет, это не сработает. Почему?

@ synchronized требуется выделенный и постоянный объект для синхронизации. Думайте об этом как об ориентире. Вы используете _instance, который изначально будет ноль (не хорошо). Приятной частью target-c является то, что классы сами являются объектами, поэтому вы можете сделать:

@ синхронизировано (сам)

Теперь, что касается дефектов, после синхронизации с постоянным объектом, который является самим классом (с использованием self), у вас будет синглтон без дефектов, однако вы будете брать на себя затраты на синхронизацию каждый раз, когда обращаетесь к своему синглтону. которые могут иметь значительные последствия для производительности.

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

Обнаружив это, мы можем обернуть ваш блок @synchronization нулевой проверкой, чтобы избежать снижения производительности после создания синглтона:

static MySingleton *_instance = nil;

+ (MySingleton *) instance
{
    if (!_instance)
    {
        @synchronized (self)
        {
            if (!_instance)
            {
                _instance = [[MySingleton alloc] init];
            }
        }
    }
    return _instance;
}

Теперь у вас есть реализация двойной проверки NULL для вашего синглтона. Но ждать! Это еще не все! Какие? Что можно оставить? Ну, для этого примера кода ничего нет, но в случае, когда есть работа, которая должна быть выполнена над синглтоном, когда он создается, нам нужно сделать некоторые соображения ...

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

if (!_instance)
{
    _instance = [[MySingleton alloc] init];
    [_instance performAdditionalPrepWork];
}

Все это выглядит великолепно, однако существует условие состязания между назначением класса, выделенного для alloc, ссылкой на _instance и точкой, в которой мы выполняем подготовительную работу. Второй поток может увидеть, что _instance существует, и использовать, если ДО подготовительной работы завершено создание условия гонки с неопределенным (и, возможно, аварийным) результатом.

Так что нам нужно делать? Что ж, нам нужно полностью подготовить синглтон, прежде чем он будет назначен на ссылку _instance.

if (!_instance)
{
    MySingleton* tmp = [[MySingleton alloc] init];
    [tmp performAdditionalPrepWork];
    _instance = tmp;
}

Легко, правда? Мы решили эту проблему, отложив присвоение синглтона для ссылки _instance до тех пор, пока мы полностью не подготовим объект, верно? В идеальном мире, да, но мы не живем в идеальном мире, и, как выясняется, нам нужно учитывать внешнее воздействие на наш идеальный код ... компилятор.

Компиляторы очень продвинуты и усердно работают над повышением эффективности кода за счет устранения кажущихся избыточностей. Избыточности, такие как использование временного указателя, которого, казалось бы, можно избежать, напрямую используя ссылку _instance. Curses эффективные компиляторы!

Это нормально, хотя для каждой платформы, имеющей эту проблему, предоставляется API низкого уровня. Мы будем использовать барьер памяти между подготовкой переменной tmp и назначением ссылки _instance, иначе OSMemoryBarrier () для платформ iOS и Mac OS X. Он просто служит индикатором для компилятора, что код перед барьером должен приниматься во внимание независимо от кода, который следует, тем самым устраняя интерпретацию компилятором избыточности кода.

Вот наш новый код в проверке нуля:

if (!_instance)
{
    MySingleton* tmp = [[MySingleton alloc] init];
    [tmp performAdditionalPrepWork];
    OSMemoryBarrier();
    _instance = tmp;
}

Джорджем, я думаю, мы его получили! Синглтон-аксессор с двойной проверкой NULL, который на 100% безопасен для потоков. Это излишне? Зависит от того, насколько важны для вас производительность и безопасность потоков. Вот последняя реализация синглтона:

#include <libker/OSAtomic.h>

static MySingleton *_instance = nil;

+ (MySingleton *) instance
{
    if (!_instance)
    {
        @synchronized (self)
        {
            if (!_instance)
            {
                MySingleton* tmp = [[MySingleton alloc] init];
                [tmp performAdditionalPrepWork];
                OSMemoryBarrier();
                _instance = tmp;
            }
        }
    }
    return _instance;
}

Теперь, если у вас нет подготовительной работы, которую нужно выполнить в target-C, просто назначить выделенный MySingleton, выделенный для alloc, просто отлично. В C ++, однако, порядок операций диктует необходимость сделать трюк временная переменная с барьером памяти. Зачем? Поскольку выделение объекта и привязка к ссылке произойдут ДО создания объекта.

_instance = new MyCPPSingleton(someInitParam);

(эффективно) совпадает с

_instance = (MyCCPSingleton*)malloc(sizeof(MyCPPSingleton));  // allocating the memory
_instance->MyCPPSingleton(someInitParam);                     // calling the constructor

Так что, если вы никогда не используете C ++, не берите в голову, но если вы это делаете - обязательно имейте это в виду, если вы планируете применить синглтон с двойной проверкой NULL в C ++.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...