24

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

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

GCD Reference,我不清楚。

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

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

4

3 回答 3

62

dispatch_once_t不能是实例变量。

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

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

于 2013-11-07T19:43:59.867 回答
19

11 月 16 日更新

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

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

我似乎不同意格雷格帕克,但可能不是真的......

好吧,格雷格和我似乎不同意我们是否不同意,或者答案,还是什么;-) 所以我正在更新我 2016 年 8 月的答案,提供更详细的答案基础,为什么它可能是错误的,如果是这样,如何修复它(所以原始问题的答案仍然是“是”)。希望格雷格和我要么同意,要么我会学到一些东西——结果都是好的!

所以首先是 8 月 16 日的答案,然后是对答案基础的解释。为了避免混淆,原来的游戏已经被删除,历史的学生可以查看编辑轨迹。


答案:2016 年 8 月

我似乎不同意格雷格帕克,但可能不是真的......

原来的问题:

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

简短回答:答案是肯定的,前提是在对象的初始创建和任何使用dispatch_once.

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

With dispatch_onceApple 声明零初始化的全局变量保证为零,但在dispatch_once执行 a 之前,零初始化的实例变量(零初始化是 Objective-C 的默认值)不保证为零。

解决办法是插入内存屏障;假设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] 行没有内存屏障。在具有推测性预读和分支预测的处理器上cachedValue,行 [C] 的读取可能发生predicate在行 [A] 的读取之前,这可能导致错误的结果(对于 的错误值cachedValue

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

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

  • 之前到达线 [A] 的任何处理器dispatch_once_internal()都已被执行和变异,predicate需要0predicate. 使用初始化为零的全局或静态变量predicate将保证这一点。

就我们当前的目的而言,重要的一点是它的dispatch_once_internal 变异 predicate方式使得 [A] 行可以毫无障碍地工作。

8 月 16 日答案的详细解释:

所以我们知道使用初始化为零的全局或静态满足dispatch_once()无障碍快速路径的要求。我们也知道由dispatch_once_internal()to产生的突变predicate被正确处理。

我们需要确定的是,我们是否可以使用实例变量predicate并对其进行初始化,以使上面的 [A] 行永远无法读取其预初始化值 - 就好像它可能会破坏一样。

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

8 月 16 日答案的执行和数据流的概要是:

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.

为了能够使用实例变量作为谓词,必须不可能以这样一种方式执行步骤 9,即在步骤 1 将其归零之前读取内存中的值。

如果省略步骤 4,即没有插入适当的屏障,init则尽管处理器 2 在执行步骤 9 之前必须获得处理器 1 生成的对象引用的正确值,但(理论上)处理器 1 的零写入是可能的在步骤 1 中尚未执行/写入全局内存,处理器 2 将看不到它们。

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

然而,我们现在必须考虑投机性预读,就像必须考虑的那样dispatch_once()。处理器 2 能否在步骤 4 的屏障确保内存为零之前执行步骤 9 的读取?

考虑:

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

  • 步骤 7 无法获取对象引用,直到步骤 6 存储/传递它;

  • 直到第 5 步返回它,第 6 步才将其存储/传递;和

  • 第 5 步是在第 4 步的障碍之后...

TL;DR:第 9 步如何获得执行读取所需的对象引用,直到第 4 步包含屏障之后?(考虑到执行路径很长,有多个分支,一些有条件的(例如在方法分派中),推测性预读是否完全是一个问题?)

所以我认为第 4 步的障碍就足够了,即使存在影响第 9 步的投机性预读。

考虑格雷格的评论:

Greg 将 Apple 关于谓词的源代码注释从“必须初始化为零”加强为“必须从不非零”,这意味着从加载时间开始,这适用于初始化为零的全局变量和静态变量。该论点基于克服无障碍dispatch_once()快速路径所需的现代处理器的推测性预读。

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

让我们假设 Greg 是对的(这根本不可能!),然后我们处于 Apple 已经处理过的情况dispatch_once(),我们需要击败预读。苹果通过使用dispatch_atomic_maximally_synchronizing_barrier()屏障来做到这一点。我们可以在第 4 步使用相同的屏障,并阻止执行以下代码,直到处理器 2 的所有可能的推测性预读都被击败;并且作为以下代码,第 5 步和第 6 步必须在处理器 2 甚至有一个对象引用之前执行,它可以用来推测性地执行第 9 步,一切正常。

因此,如果我理解 Greg 的担忧,那么 usingdispatch_atomic_maximally_synchronizing_barrier()将解决它们,并且使用它而不是标准屏障不会引起问题,即使它实际上并不需要。因此,尽管我不相信这是必要的,但这样做在最坏的情况下是无害的。因此,我的结论和以前一样(强调):

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

如果我的逻辑有误,我相信 Greg 或其他读者会告诉我。我准备好面对手掌了!

当然,您必须确定适当障碍的成本是否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

如果您想知道这是如何工作的,请阅读 Apple 的源代码。

于 2012-12-13T11:12:52.143 回答
2

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

于 2012-12-13T09:13:12.567 回答