我可以将dispatch_once_t谓词声明为成员变量而不是静态吗?

时间:2012-12-13 08:45:39

标签: objective-c grand-central-dispatch

我想每个实例只运行一次代码块。

我可以将dispatch_once_t谓词声明为成员变量而不是静态变量吗?

来自GCD Reference,我不清楚。

  

谓词必须指向存储在全局或静态中的变量   范围。使用自动或动态谓词的结果   存储未定义。

我知道我可以使用dispatch_semaphore_t和布尔标志来做同样的事情。我只是好奇。

3 个答案:

答案 0 :(得分:60)

dispatch_once_t不能是实例变量。

dispatch_once()的实施要求dispatch_once_t为零,而从未为非零。以前不是零的情况需要额外的内存屏障才能正常工作,但dispatch_once()出于性能原因省略了这些障碍。

实例变量初始化为零,但它们的内存可能先前已存储了另一个值。这使得dispatch_once()使用它们不安全。

答案 1 :(得分:19)

11月16日更新

这个问题最初是在2012年用“娱乐”回答的,它没有声称提供明确的答案,并对此提出了警告。事后看来,这种娱乐应该是私密的,尽管有些人喜欢它。

2016年8月,这个Q& A引起了我的注意,我提供了一个正确的答案。在那写道:

  

我似乎不同意Greg Parker,但可能不是真的......

好吧,Greg和我似乎不同意我们是否不同意,或者答案,或者其他什么;-)所以我更新了2016年8月的答案,更详细地回答了答案,为什么可能< / em>是错的,如果是的话如何解决它(所以对原始问题的答案仍然是&#34;是&#34;)。希望Greg&amp;我会同意,或者我会学到一些东西 - 结果都很好!

首先是8月16日答案,然后解释答案的基础。原来的娱乐活动已被删除,以避免任何混淆,历史学生可以查看编辑线索。


答案:2016年8月

我似乎不同意Greg Parker,但可能不是真的......

最初的问题:

  

我可以将dispatch_once_t谓词声明为成员变量而不是静态变量吗?

简答:答案是肯定的提供在初始创建对象和使用dispatch_once之间存在内存障碍。

快速说明: dispatch_once_t dispatch_once变量的要求是它必须最初为零。困难来自现代多处理器上的内存重新排序操作。虽然可能看起来已经根据程序文本(高级语言或汇编程序级别)执行了到某个位置的存储,但是实际存储可以被重新排序并且在随后读取相同位置之后发生。为了解决这个内存障碍,可以使用来强制所有在它们之前发生的内存操作在跟随它们之前完成。 Apple提供OSMemoryBarrier()来执行此操作。

使用dispatch_once Apple声明零初始化全局变量保证为零,但零初始化实例变量(以及零初始化是此处的Objective-C默认值)不保证为零在dispatch_once执行之前。

解决方案是插入内存屏障;假设dispatch_once出现在实例的某个成员方法中,放置此内存屏障的明显位置在init方法中,因为(1)它只会被执行一次(每个实例)并且(2)init必须在任何其他成员方法被调用之前返回。

所以,是的,通过适当的内存屏障,dispatch_once可以与实例变量一起使用。


2016年11月

序言:有关dispatch_once

的说明

这些说明基于Apple的代码和dispatch_once的评论。

dispatch_once的用法遵循标准模式:

id cachedValue;
dispatch_once_t predicate = 0;
...
dispatch_once(&predicate, ^{ cachedValue = expensiveComputation(); });
... use cachedValue ...

并且最后两行被扩展为内联dispatch_once是一个宏),类似于:

if (predicate != ~0) // (all 1's, indicates the block has been executed)  [A]
{
    dispatch_once_internal(&predicate, block);                         // [B]
}
... use cachedValue ...                                                // [C]

注意:

  • Apple的消息来源声明predicate必须初始化为零,并注意到全局和静态变量默认为零初始化。

  • 请注意,在第[A]行,没有内存屏障。在具有推测性预读和分支预测的处理器上,行[C]中的cachedValue读取可能在读取行[A]中的predicate之前发生,这可能导致错误的结果(错误cachedValue

  • 的值
  • 可以使用屏障来防止这种情况发生,但是这种情况很慢,并且Apple希望在已经执行过一次阻止的常见情况下这种速度很快,所以......

  • dispatch_once_internal,行[B],在内部使用障碍和原子操作,使用特殊屏障dispatch_atomic_maximally_synchronizing_barrier()来击败推测预读,因此允许行[A]无障碍,因此很快。

  • 执行dispatch_once_internal() predicate之前到达第[A]行的所有处理器0需要从predicate读取predicate。使用dispatch_once_internal初始化为零的全局或静态将保证这一点。

对于我们当前的目的而言,重要的一点是,predicate 改变 dispatch_once(),使得行[A]在没有的情况下工作屏障。

8月16日的长解释答案:

因此我们知道使用初始化为零的全局或静态符合dispatch_once_internal()无障碍快速路径的要求。我们也知道predicatepredicate所做的突变得到了正确处理。

我们需要确定的是,我们是否可以为Processor 1 Processor 2 0. Call alloc 1. Zero instance var used for predicate 2. Return object ref from alloc 3. Call init passing object ref 4. Perform barrier 5. Return object ref from init 6. Store or send object ref somewhere ... 7. Obtain object ref 8. Call instance method passing obj ref 9. In called instance method dispatch_once tests predicate, This read is dependent on passed obj ref. 使用实例变量,并以上述行[A]永远无法读取其预先初始化值的方式对其进行初始化 - 好像它可能会破坏。

我的8月16日回答说这是可能的。为了理解这一点,我们需要考虑具有推测性预读的多处理器环境中的程序和数据流。

8月16日答案的执行和数据流的大纲是:

init

为了能够使用实例变量作为谓词,那么在步骤1将其归零之前,必须以这样的方式执行步骤9才能读取内存中的值。< / p>

如果省略步骤4,即在dispatch_once()中没有插入适当的屏障,那么虽然处理器2必须获得处理器1生成的对象引用的正确值,然后才能执行步骤9,但它是(从理论上讲,处理器1在步骤1中的零写操作尚未执行/写入全局存储器,处理器2也不会看到它们。

因此,我们插入第4步并执行障碍。

然而,我们现在必须考虑推测预读,就像dispatch_once()必须这样。处理器2可以在步骤4的屏障确保存储器为零之前执行步骤9的读取吗?

考虑:

  • 处理器2无法推测或以其他方式执行步骤9的读取,直到它具有在步骤7中获得的对象引用 - 并且这样做推测性地要求处理器确定步骤8中的方法调用,其中Objective-C中的目标是动态确定的,最终会在包含步骤9的方法中结束,这是非常先进的(但并非不可能)推测;

  • 在步骤6存储/传递之前,步骤7无法获取对象引用;

  • 步骤6还没有存储/通过,直到第5步返回它为止;以及

  • 步骤5在步骤4的屏障之后......

TL; DR :步骤9如何具有执行读取所需的对象引用,直到包含屏障的步骤4之后? (并且考虑到具有多个分支的长执行路径,一些条件(例如内部方法调度),是推测性预读一个问题吗?)

所以我认为步骤4中的障碍是足够的,即使存在推测预读影响步骤9。

考虑Greg的评论:

Greg加强了Apple关于来自&#34的谓词的源代码评论;必须初始化为零&#34; to&#34;必须永远不是非零&#34;,这意味着自加载时间起,对于初始化为零的全局变量和静态变量,这只 为真。该论点基于打败无障碍dispatch_once()快速路径所需的现代处理器的推测性预读。

实例变量在对象创建时初始化为零,并且它们占用的内存在此之前可能不为零。然而,如上所述,可以使用合适的屏障来确保dispatch_once()不读取预初始化值。我认为格雷格不同意我的论点,如果我正确地听取他的意见,并认为第4步的障碍不足以处理推测性预读。

让我们假设Greg是对的(这根本不可能!),那么我们处于Apple已经在dispatch_atomic_maximally_synchronizing_barrier()处理过的情况,我们需要打败预读。 Apple通过使用dispatch_atomic_maximally_synchronizing_barrier()屏障来做到这一点。我们可以在步骤4中使用相同的屏障,并防止执行以下代码,直到处理器2的所有可能的推测性读取都被取消;并且如下面的代码,步骤5&amp; 6,必须在处理器2之前执行甚至有一个对象引用它可以用来推测性地执行步骤9一切正常。

因此,如果我理解Greg的问题,那么使用dispatch_once将解决这些问题,即使实际上并不需要,使用它而不是标准屏障也不会导致问题。因此,虽然我不相信它是必要,但最糟糕的是无害。因此,我的结论仍然如此(强调补充):

  

所以,是的,使用适当的内存屏障,init可以与实例变量一起使用。

我确定格雷格或其他一些读者会告诉我我的逻辑是否错误。我随时准备好面子!

当然,您必须确定dispatch_once()适当的障碍的成本是否值得您使用dispatch_atomic_maximally_synchronizing_barrier()获取每次实例一次行为所获得的好处或者你是否应该以另一种方式满足你的要求 - 这些替代方案超出了这个答案的范围!

dispatch_atomic_maximally_synchronizing_barrier()的代码:

根据Apple的来源改编的#if defined(__x86_64__) || defined(__i386__) #define dispatch_atomic_maximally_synchronizing_barrier() \ ({ unsigned long _clbr; __asm__ __volatile__( "cpuid" : "=a" (_clbr) : "0" (0) : "ebx", "ecx", "edx", "cc", "memory"); }) #else #define dispatch_atomic_maximally_synchronizing_barrier() \ ({ __c11_atomic_thread_fence(dispatch_atomic_memory_order_seq_cst); }) #endif 的定义,您可以在自己的代码中使用:

{{1}}

如果您想了解其工作原理,请阅读Apple的源代码。

答案 2 :(得分:2)

你引用的引用似乎很清楚:谓词必须在全局或静态范围内,如果你将它用作成员变量,它将是动态的,因此结果将是未定义的。所以不,你不能。 dispatch_once()不是您正在寻找的内容(参考文献还说:在应用程序的生命周期内执行一次 的块对象,这是不是你想要的,因为你希望这个块为每个实例执行。)