iOS的这个单例实现的任何潜在缺陷?

时间:2012-02-27 02:23:53

标签: objective-c ios singleton

最直接,最简单的实现就是这个

static MySingleton *_instance = nil;

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

        return _instance;
    }
}

其实我知道几个关于单身人士的热门帖子 Implementing a Singleton in iOS 和流行template

所以我的问题是“上述实施的任何缺陷”?

2 个答案:

答案 0 :(得分:5)

是的,您的实施存在很大缺陷。 @synchronized指令变为对objc_sync_enter的调用以及稍后调用objc_sync_exit。当您第一次调用instance方法时,_instancenilobjc_sync_enter功能在您nil传递instance时不会锁定,如looking at its source code所示。

因此,如果两个线程在初始化_instance之前同时调用MySingleton,您将创建两个_instance实例。

此外,您应该将dispatch_once变量放在函数中,除非您有理由将其公开给整个源文件。

iOS 4.0及更高版本的单例访问器的首选实现使用非常高效的+ (MySingleton *)sharedInstance { static MySingleton *theInstance; static dispatch_once_t once; dispatch_once(&once, ^{ theInstance = [[self alloc] init]; }); return theInstance; } 函数,如下所示:

dispatch_once

在iOS 4.0之前,@synchronized功能不可用,因此如果您确实需要支持较旧的iOS版本(不太可能),则必须使用效率较低的nil。由于无法在+ (MySingleton *)sharedInstance { static volatile MySingleton *theInstance; if (!theInstance) { @synchronized (self) { if (!theInstance) theInstance = [[self alloc] init]; } } return theInstance; } 上进行同步,因此可以在类对象上进行同步:

{{1}}

答案 1 :(得分:1)

What should my Objective-C singleton look like?对单身人士来说是一个很好的讨论(正如乔希指出的那样),但要回答你的问题:

不,那不行。为什么呢?

@synchronized需要一个已分配的常量对象来进行同步。把它想象成一个参考点。你正在使用_instance,它最初将是零(不好)。 objective-c的一个很好的部分是类本身就是对象,所以你可以这样做:

@同步(自)

现在就缺陷而言,一旦你对作为类本身的常量对象(使用self)进行同步,你将拥有一个无缺陷的单例,但是每次访问你的单例时你都会承担同步开销的代价。这可能会产生重大的性能影响。

缓解这种情况的简单机制是确定创建只需要发生一次,然后所有后续调用将返回相同的引用,该引用在进程生命周期中永远不会改变。

识别这个,我们可以将你的@synchronization块包装成nil检查,以避免在创建单例之后出现性能损失:

static MySingleton *_instance = nil;

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

现在你有一个双重NULL检查实现你的单身人士。可是等等!还有更多要考虑的事情!什么?什么可以留下?对于这个示例代码,没什么,但是在单例创建工作的情况下,我们需要做一些考虑......

让我们看看第二个零检查,并为需要额外工作的常见单例场景添加一些额外的代码。

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

这一切看起来都很棒,但是在allocininited类分配给_instance引用和我们执行准备工作的点之间存在竞争条件。第二个线程可以看到_instance存在并且如果在准备工作完成之前使用,则创建具有未定义(并且可能崩溃)结果的竞争条件。

那么我们需要做什么?好吧,我们需要在分配给_instance引用之前完全准备好单例。

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

简单,对吗?我们通过将单例的赋值延迟到_instance引用来解决问题,直到我们完全准备好对象为止,对吧?在一个完美的世界中,是的,但我们并没有生活在一个完美的世界中,因为事实证明我们需要考虑一个外部的力量,用我们完美的代码......编译器。

编译器非常先进,通过消除看似冗余来努力提高代码效率。冗余就像使用临时指针一样,可以通过直接使用_instance引用来避免。诅咒高效的编译器!

没关系,每个平台都有一个低级API,但是有这个问题。我们将在tmp变量的准备和_instance引用(用于iOS和Mac OS X平台的OSMemoryBarrier())的分配之间使用内存屏障。它只是作为编译器的指示器,应该独立于后面的代码考虑屏障之前的代码,从而消除编译器对代码冗余的解释。

这是我们在零检查中的新代码:

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

现在,如果你没有在objective-C中做准备工作,那么只需分配alloc-inited MySingleton即可。但是,在C ++中,操作顺序要求必须使用临时变量和内存屏障技巧。为什么?因为对象的分配和对引用的赋值将在构造对象之前发生。

_instance = new MyCPPSingleton(someInitParam);

(有效)与

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

因此,如果您从未使用过C ++,请不要使用C ++,但如果您这样做 - 请务必在计划在C ++中应用双NULL检查单例时牢记这一点。