问题 我们为什么要分开alloc和init调用以避免Objective-C中的死锁?


在阅读有关线程安全的单身人士时,我发现了 线程安全实例化单例 在这里,以及在接受的答案中这段代码:

    sharedInstance = [MyClass alloc];
    sharedInstance = [sharedInstance init];

我们为什么要分离alloc和init方法?答案的作者写道:

即,如果 init 被分配的类碰巧调用了 sharedInstance 方法,它将在设置变量之前执行此操作。在这两种情况下都会导致僵局。这是你想要分开的一次 alloc 和 init

有人可以详细向我解释这种分离的好处是什么?我无法理解作者的意思。我真的需要分开吗? alloc 和 init 我创建一个单例时调用方法,即使我这样做 dispatch_once() 这是线程安全??


8399
2018-01-03 01:51


起源

bbum正在制造的那点是 dispatch_once() 是“线程安全的”,因为只有一个线程可以运行Block。危险在于 dispatch_once() 不能从它的Block再次调用 - 事实并非如此 折返。 bbum的代码 - 正如他最后指出的那样 - 实际上并没有阻止这个问题。他对这个问题了解得多,但我认为他可能对这种特殊的僵局机会过于偏执。我想重新打电话给 sharedInstance 方法来自 init 可以认为程序员错误。 - Josh Caswell
@JoshCaswell“危险的是dispatch_once()无法从其Block再次调用”我真的不明白为什么我们应该这样做呢? - Oleksandr Karaberov
我在原始问题中通知了bbum。所以我希望他能增加关于这种“偏执”分离的更多信息 - Oleksandr Karaberov
@AlexanderKaraberov更正,如果有任何内部运行的代码 -init 打电话给 +sharedInstance 使用时 dispatch_once(),这将导致死锁。只要你不这样做,你是否分开并不重要 +alloc/-init 或者把它们放在一起请参阅我的回答以获取更多信息 - indragie
这太荒谬了 - 将两个调用放在一行或两行之间没有任何功能差异,除了使它在两行中通过“sharedInstance”变量(应该是自动的和本地的,因此“安全”)暴露出非对象的对象。 )。 - Hot Licks


答案:


@bbum的帖子 已更新,以提及 该解决方案不能解决所描述的问题。无论你是否分开 +alloc 和 -init 或不,这个问题仍然存在。

理由是在编辑他的帖子,但要扩展, dispatch_once() 不是 折返。在这种情况下,这意味着调用 dispatch_once()   一个 dispatch_once() 阻塞(即递归)将导致死锁。

例如,如果您有以下代码 +sharedInstance

+ (MyClass *)sharedInstance
{   
    static MyClass *sharedInstance = nil;
    static dispatch_once_t pred;

    dispatch_once(&pred, ^{
        sharedInstance = [[MyClass alloc] init]
    });

    return sharedInstance;
}

..和 MyClass-init 方法直接或间接也调用自己的方法 +sharedInstance 类方法(例如可能是其他一些对象) MyClass -init 通过分配呼叫 MyClass+sharedInstance),这意味着你试图打电话 dispatch_once 从内部来看。

以来 dispatch_once 是线程安全的,同步的和设计的,以便它执行 一次,你不能调用 dispatch_once 在块内部完成执行一次之前再次执行。这样做会导致死锁,因为第二次调用 dispatch_once 将等待第一次调用(已经在执行中)完成,而第一次调用正在等待第二次(递归)调用 dispatch_once 通过。他们互相等待,因此陷入僵局。

如果你想要一个提供重入的解决方案,你需要使用类似的东西 NSRecursiveLock 这比相当贵 dispatch_once,它不使用锁定机制。

编辑:推理分裂 +alloc/-init 在@ bbum的原始答案中:

在编辑之前发布的原始代码@bbum看起来像这样:

+ (MyClass *)sharedInstance
{   
    static MyClass *sharedInstance = nil;
    static dispatch_once_t pred;

    if (sharedInstance) return sharedInstance;

    dispatch_once(&pred, ^{
        sharedInstance = [MyClass alloc];
        sharedInstance = [sharedInstance init];
    });

    return sharedInstance;
}

注意这一行: if (sharedInstance) return sharedInstance;

这里的想法是指定一个非零值 sharedInstance 在打电话之前 -init 会导致现有的价值 sharedInstance (从...返回 +alloc)被退回 之前 击中了 dispatch_once() 在这种情况下调用(并避免死锁) -init 调用导致递归调用 +sharedInstance 正如我在回答中所讨论的那样。

但是,这是一个脆弱的解决方案,因为 if 声明没有线程安全。


16
2018-01-03 02:16



好的。+ 1.这澄清了很多。但这句话让我感到烦恼:'这个解决方案不再解决所描述的问题:How can it solve the problem earlier?. Words 不再说了。从那以后,对init的行为有什么改变吗? (2010)? - Oleksandr Karaberov
还有一个问题。你说:“如果你想要一个提供可重入性的解决方案,你需要使用类似NSRecursiveLock的东西,它比dispatch_once贵得多,后者不使用锁定机制。”但是我们在同步方法中使用这个锁,为什么它会很昂贵呢?我们可以在编辑线程中使用if语句吗? - Oleksandr Karaberov
@AlexanderKaraberov dispatch_once() 解决方案旨在保证线程安全。如果你想要类似的线程安全 同 重入,就是这样 NSRecursiveLock 是为了。如果你既不使用锁也不使用 dispatch_once(), +sharedInstance 不再是线程安全的,你必须确保不要同时在多个线程上调用它。我说 NSRecursiveLock 更贵,因为 NSRecursiveLock 实际上使用锁定机制而 dispatch_once() 才不是。使用锁比不使用锁更昂贵。 - indragie
这种拆分实现似乎是非常危险的代码。如果第二个线程之前访问sharedInstance init 已经完成它将获得 未初始化 目的。这不可能是善良。 - Hot Licks
@HotLicks是正确的;这是危险代码 现在但是,当我们在90年代初采用这种模式时,并不认为是危险的,因为NeXTSTEP程序很少使用超出本地化计算引擎的线程。几乎没有什么要求确保线程安全 sharedInstance因为当应用程序已经多线程时,甚至会调用第一次调用是非常罕见的(基金会通知有理由指示应用程序何时“多线程”)。不同的时代。 - bbum