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

tee*_*pap 24 objective-c grand-central-dispatch

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

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

GCD参考资料中,我不清楚.

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

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

Gre*_*ker 60

dispatch_once_t 不能是实例变量.

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

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

  • 不对.所有Objective-C实例变量都保证为零(除了可以使用零arg就地构造函数初始化的C++类型的ivars). (8认同)
  • 在任何情况下,上述初始化都不足以用于`dispatch_once`使用,因为现在变量的存储在过程的生命周期中可能早于非零. (4认同)
  • ARC增加了对ARC管理的对象指针类型的*local*变量的零初始化. (3认同)
  • 可以通过在对象的init方法的末尾添加OSMemoryBarrier()来解决这些限制吗? (2认同)

CRD*_*CRD 19

11月16日更新

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

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

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

好吧,看起来格雷格和我不同意我们是否不同意,或答案,或者其他什么;-)所以我更新了我的2016年8月答案,答案更详细,为什么它可能是错的,如果是这样的话解决它(所以原始问题的答案仍然是"是").希望Greg和我要么同意,要么我会学到一些东西 - 要么结果好!

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


答案:2016年8月

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

原来的问题:

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

答案很简单:答案是肯定的提供,有初始创建的对象和任何使用之间的内存屏障dispatch_once.

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

随着dispatch_once苹果公司指出,零初始化的全局变量保证是零,但零初始化的实例变量(和零正开始是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 ...
Run Code Online (Sandbox Code Playgroud)

最后两行是内联扩展(dispatch_once是一个宏),如下所示:

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

笔记:

  • Apple的源代码predicate必须初始化为零,并指出全局和静态变量默认为零初始化.

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

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

  • dispatch_once_internal,行[B],内部使用障碍和原子操作,使用特殊屏障,dispatch_atomic_maximally_synchronizing_barrier()以打败推测预读,因此允许线[A]无障碍,因此快速.

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

对于我们目前的目的而言,重要的一点就是以这样一种方式进行dispatch_once_internal 变异 predicate:线[A]在没有任何障碍的情况下工作.

8月16日的长解释答案:

所以我们知道使用全局或静态初始化为零符合dispatch_once()无障碍快速路径的要求.我们也知道,所作的突变dispatch_once_internal(),以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.
Run Code Online (Sandbox Code Playgroud)

为了能够使用实例变量作为谓词,那么必须以这样的方式执行步骤9,使得它步骤1将其归零之前读取存储器中的值.

如果省略步骤4,即在init那时没有插入适当的屏障,尽管处理器2必须在处理器1执行步骤9之前获得由处理器1生成的对象参考的正确值,但理论上可能是处理器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之后?(并且考虑到长执行路径,有多个分支,一些条件(例如内部方法调度),是推测预读一个问题吗?)

因此,我认为即使存在推测性预读影响步骤9,步骤4中的障碍也是足够的.

考虑格雷格的评论:

Greg强化了Apple关于谓词的源代码注释,从"必须初始化为零"到"绝不能非零",这意味着自加载时间以来,这适用于初始化为零的全局变量和静态变量.该论点基于打败无障碍dispatch_once()快速路径所需的现代处理器的推测性预读.

实例变量在对象创建时初始化为零,并且它们占用的内存在此之前可能不为零.然而,如上所述,可以使用合适的屏障来确保dispatch_once()不读取预初始化值.我认为格雷格不同意我的论点,如果我正确地听取他的意见,并认为第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可以与实例变量一起使用.

如果我的逻辑错误,我肯定格雷格或其他读者会告诉我.我准备好面对面!

当然,您必须确定适当障碍的成本是否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
Run Code Online (Sandbox Code Playgroud)

如果你想知道它是如何工作的,请阅读Apple的源代码.

  • 这是不正确的.要求不是dispatch_once_t最初为零,而是dispatch_once_t从未为非零.打破这种假设可能在大多数时间都有效,但如果你运气不好,那么这个区块可能会执行多次或根本不执行. (4认同)
  • 有些人可能会认为查看`dispatch_once`的实现更有意思:[trunk/src/once.c](http://libdispatch.macosforge.org/trac/browser/trunk/src/once.c) (3认同)

Gui*_*ume 2

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

  • 我没有注意到“应用程序的生命周期”阶段。然而,它可以指应用程序的生命周期内的每个实例。 (2认同)