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

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

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

从GCD参考 ,我不清楚。

谓词必须指向存储在全局或静态作用域中的variables。 使用带有自动或dynamic存储的谓词的结果是未定义的。

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

dispatch_once_t不能是一个实例variables。

dispatch_once()的实现要求dispatch_once_t为零,并且从不为零 。 之前的非零案例需要额外的内存屏障才能正常工作,但dispatch_once()因性能原因而忽略了这些障碍。

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

11月16日更新

这个问题最初是在2012年以“娱乐”的方式回答的,但并没有提供明确的答案,并提出了一个警告。 事后看来,这样的娱乐应该可以保持私密,尽pipe有些人喜欢它。

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

我会看起来不同意格雷格帕克,但可能不是真的…

那么格雷格和我似乎不同意我们是否不同意,或答案,或什么;-)所以我更新我2016年8月的答案与更详细的答案,为什么它可能是错误的,如果是的话如何修复它(所以对原始问题的答案仍然是“是”)。 希望格雷格和我会同意,或者我会学到一些东西 – 要么结果是好的!

因此,首先8月16日的答案是这样的,然后解释答案的基础。 为了避免混淆,最初的娱乐已被删除,历史的学生可以查看编辑轨迹。


答案:2016年8月

我会看起来不同意格雷格帕克,但可能不是真的…

原来的问题:

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

简答答案是肯定的在初始创build对象和使用dispatch_once之间有一个内存屏障。

快速说明:dispatch_once_tvariables的要求是它必须初始为零。 困难来自现代多处理器的存储器重新sorting操作。 虽然根据程序文本(高级语言或汇编程序级别)已经执行了对某个位置的存储,但是实际存储可能会被重新sorting,并随后读取相同位置之后发生。 为了解决这个内存障碍,可以使用所有的内存操作在他们之前完成之前完成。 苹果提供了OSMemoryBarrier()来做到这一点。

对于dispatch_once Apple声明零初始化的全局variables保证为零,但在执行dispatch_once之前,零初始化的实例variables(和零初始化是Objective-C的默认值)不能保证为零。

解决的办法是插入一个内存屏障; 假设dispatch_once发生在实例的某些成员方法中,放置这个内存屏障的显而易见的地方是init方法,因为(1)它只会被执行一次(每个实例)和(2) init必须先返回任何其他的成员方法都可以调用。

所以是的,有了合适的内存屏障, dispatch_once可以和一个实例variables一起使用。


2016年11月

序言:关于dispatch_once注释

这些笔记是基于苹果的代码和dispatch_once意见。

dispatch_once用法遵循标准模式:

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

和最后两行是内联dispatch_once是一个macros)扩展到像这样的东西:

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

笔记:

  • 苹果公司的消息来源指出, predicate必须初始化为零,并注意全局和静态variables默认为零初始化。

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

  • 一个障碍可以用来防止这种情况,但是这是缓慢的,苹果希望这是快速的常见的情况下,曾经块已经执行,所以…

  • dispatch_once_internal内部使用障碍和primefaces操作的dispatch_once_internal ,line [B]使用特殊的障碍dispatch_atomic_maximally_synchronizing_barrier()来击败推测性预读,因此允许行[A]是无障碍的并因此是快速的。

  • dispatch_once_internal()之前到达行[A]的任何处理器已经被执行,并且被突变的predicate需要从predicate读取0 。 使用全局或静态初始化为predicate为零将保证这一点。

对于我们目前的目的来说,重要的一点是dispatch_once_internal predicate的方式使行[A] 没有任何障碍地工作。

8月16日长解释答案:

所以我们知道使用全局或静态初始化为零符合dispatch_once()的无障碍快捷path的要求。 我们也知道dispatch_once_internal()predicate所做的变异是正确处理的。

我们需要确定的是,我们是否可以使用一个实例variables作为predicate ,并以这样一种方式初始化它,以使上面的行[A]永远不会读取其预先初始化的值 – 就好像它可能会破坏一样。

我8月16日答案说这是可能的。 为了理解这个基础,我们需要考虑多处理器环境下的程序和数据stream,并进行推测性预读。

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

 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. 

为了能够使用一个实例variables作为谓词,那么执行步骤9是不可能的,以至于步骤1将它清零之前它读取存储器中的值。

如果省略步骤4,也就是说,在init没有插入适当的屏障,那么虽然处理器2在执行步骤9之前必须获得处理器1生成的对象引用的正确值,但理论上可能的是,处理器1的零在第1步写入尚未执行/写入全局内存和处理器2将不会看到它们。

所以我们插入第4步并执行屏障。

不过,我们现在必须考虑推测预读,就像dispatch_once()所做的那样。 在步骤4的屏障确保内存为零之前,处理器2是否可以执行步骤9的读取?

考虑:

  • 处理器2不能执行步骤9的读取,直到其具有在步骤7中获得的对象参考 – 并且这样做推测性地要求处理器确定在步骤8中的方法调用,其在Objective-C中的目的地是dynamic确定,将最终在包含步骤9的方法,这是相当先进(但不是不可能)的推测;

  • 步骤7不能获得对象引用,直到步骤6已经存储/传递它为止;

  • 步骤6没有得到它存储/通过,直到步骤5已经返回它; 和

  • 步骤5在步骤4的障碍之后…

TL; DR :第9步如何执行读取所需的对象引用,直到第4步包含屏障? (考虑到长执行path,多分支,一些条件(如内部方法调度),是推测预读一个问题呢?)

所以我认为,即使在存在推测性预读的步骤9的情况下,步骤4中的障碍也是足够的。

考虑格雷格的评论:

Greg强化了苹果关于谓词从“必须被初始化为零”到“绝不能是非零”的谓词的源代码评论,这意味着自加载时间以来, 这只对全局和静态variables初始化为零是正确的。 这个论点是基于无障碍dispatch_once()快速path所需的现代处理器的预测性预读。

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

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

因此,如果我了解Greg的担忧,那么使用dispatch_atomic_maximally_synchronizing_barrier()将解决这些问题,而使用它而不是标准屏障即使不是真正需要也不会引起问题。 所以尽pipe我不相信这是必要的,但最坏的情况是这样做的。 因此,我的结论与以前一样(强调增加):

所以是的,有了合适的内存屏障, dispatch_once可以和一个实例variables一起使用。

我敢肯定,如果我的逻辑错误,格雷格或其他读者会让我知道。 我准备好面对面!

当然,您必须决定init 适当屏障的成本是否值得您从使用dispatch_once()获取一次一次实例行为所获得的收益,或者您是否应该以另一种方式来满足您的需求 – 而且这种替代scheme不在这个答案的范围!

dispatch_atomic_maximally_synchronizing_barrier()代码:

您可以在自己的代码中使用来自Apple源代码的dispatch_atomic_maximally_synchronizing_barrier()定义:

 #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 

如果你想知道这是如何工作阅读苹果的源代码。

你引用的引用看起来很清楚:谓词必须在全局或静态范围内,如果将它用作成员variables,它将是dynamic的,所以结果将是未定义的。 所以不,你不能。 dispatch_once()不是你正在寻找的(引用也是这样的: 在一个应用程序的生命周期中 只执行一次块对象 ,这不是你想要的,因为你希望这个块为每个实例执行)。