【问题标题】:Can I declare dispatch_once_t predicate as a member variable instead of static?我可以将 dispatch_once_t 谓词声明为成员变量而不是静态的吗?
【发布时间】:2012-12-13 08:45:38
【问题描述】:

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

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

来自GCD Reference,我不清楚。

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

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

【问题讨论】:

  • 只是好奇:你能在代码中引用我一个示例,如何使用 dispatch_semaphore_t 和 BOOL 标志来做同样的事情吗?提前致谢。

标签: objective-c grand-central-dispatch


【解决方案1】:

dispatch_once_t 不能是实例变量。

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

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

【讨论】:

  • 能否通过在对象的 init 方法末尾添加 OSMemoryBarrier() 来解决这些限制?
  • 只有指针实例变量(并且可能只有对象指针实例变量),甚至只有在 ARC 下,才能保证初始化为零。 dispatch_once_t 是 long 的同义词,因此不保证初始化为零。
  • 不正确。所有 Objective-C 实例变量都保证为零(C++ 类型的 ivars 除外,可以使用它们的零参数就地构造函数初始化)。
  • ARC 对 ARC 管理的对象指针类型的 local 变量添加零初始化。
  • 在任何情况下,上述初始化都不足以供 dispatch_once 使用,因为现在变量的存储空间在进程生命周期的早期可能是非零的。
【解决方案2】:

11 月 16 日更新

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

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

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

好吧,Greg 和我似乎在我们是否不同意、答案或其他问题上存在分歧;-) 所以我正在更新我 2016 年 8 月的答案,并提供更详细的答案基础,为什么它可能 是错误的,如果是,如何解决(所以原始问题的答案仍然是“是”)。希望 Greg 和我会同意,或者我会学到一些东西 - 任何一个结果都是好的!

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


答案:2016 年 8 月

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

原问题:

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

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

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

对于dispatch_once,Apple 声明零初始化的全局变量保证为零,但零初始化的实例变量(这里的 Objective-C 默认设置为零)不保证在 a dispatch_once 被执行。

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

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


2016 年 11 月

序言:关于dispatch_once的注释

这些注释基于 Apple 的代码和 dispatch_once 的 cmets。

dispatch_once 的使用遵循标准模式:

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

最后两行被扩展为 inline(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 之前,这可能导致错误的结果(@987654343 的错误值@)

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

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

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

就我们当前的目的而言,重要的一点是 dispatch_once_internal 变异 predicate 使 [A] 行没有任何障碍。 p>

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

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

我们需要确定的是我们是否可以为predicate 使用一个instance 变量并以这样的方式对其进行初始化,使得上面的[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的barrier确保内存为零之前执行步骤9的读取?

考虑:

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

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

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

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

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

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

Greg 的 cmets 的考虑:

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

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

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

因此,如果我理解 Greg 的担忧,那么使用 dispatch_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 的源代码。

【讨论】:

  • 感谢您的测试。我投票赞成你的答案。看来我得去苹果论坛问问才能确定答案了。
  • @teerapap - dispatch_once_t的定义注释为:/* @typedef dispatch_once_t @abstract A predicate for use with dispatch_once(). It must be initialized to zero. Note: static and global variables default to zero. */;这几乎回答了这个问题。我也许应该早点查看该评论,但弄清楚很有趣......
  • 有些人可能觉得看dispatch_once的实现更有趣:trunk/src/once.c
  • 这是不正确的。要求不是 dispatch_once_t 最初为零,而是 dispatch_once_t 从未非零。打破这个假设可能在大多数情况下都有效,但如果你不走运,那么该块可能会执行多次或根本不执行。
  • @GregParker - 有趣。所以标题中的语句(“它必须初始化为零。”)不准确/不精确?鉴于内存位置在其历史上的某个时间点非零(它不是全新的 RAM),时间什么时候开始?我可以冒险回答,但你会知道 :-) 只是好奇。
【解决方案3】:

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

【讨论】:

  • 我没有注意到“应用程序生命周期”阶段。但是,可以在应用程序的整个生命周期中表示每个实例。
猜你喜欢
  • 2016-12-22
  • 2013-07-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-06
  • 1970-01-01
相关资源
最近更新 更多