每个对象使用dispatch_once_t而不是每个类

Avn*_*arr 10 singleton objective-c grand-central-dispatch

有多个源调用特定方法,但我想确保它被调用一次(每个对象)

我想使用类似的语法

// method called possibly from multiple places (threads)
-(void)finish
{

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self _finishOnce]; // should happen once per object
    });
}
// should only happen once per object
-(void)_finishOnce{...}
Run Code Online (Sandbox Code Playgroud)

问题是令牌是在同一个类的所有实例中共享的 - 所以不是一个好的解决方案 - 每个对象是否有dispatch_once_t - 如果不是,确保它被调用一次的最佳方法是什么?

编辑:

这是我想到的一个提议的解决方案 - 它看起来好吗?

@interface MyClass;

@property (nonatomic,strong) dispatch_queue_t dispatchOnceSerialQueue; // a serial queue for ordering of query to a ivar

@property (nonatomic) BOOL didRunExactlyOnceToken;

@end

@implementation MyClass

-(void)runExactlyOnceMethod
{
  __block BOOL didAlreadyRun = NO;
  dispatch_sync(self.dispatchOnceSerialQueue, ^{
     didAlreadyRun = _didRunExactlyOnceToken;
     if (_didRunExactlyOnceToken == NO) {
        _didRunExactlyOnceToken = YES;
     }
  });
  if (didAlreadyRun == YES)
  {
    return;
  }
  // do some work once
}
Run Code Online (Sandbox Code Playgroud)

ipm*_*mcc 9

正如在类似问题链接答案中所提到的,参考文档说:

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

该答案中列举了整体关注点.也就是说,它可以使它工作.详细说明:这里的关注点是谓词的存储在初始化时可靠地归零.使用静态/全局语义,这是非常有保证的.现在我知道你在想什么,"......但是,Objective-C对象也会在init上归零!",你一般都是正确的.问题出在哪里是读/写重新排序.某些体系结构(即ARM)另一个线程尝试读取令牌具有弱一致性内存模型,这意味着只要保留执行一致性的主要线程的原始意图,就可以重新排序内存读/写.在这种情况下,重新排序可能会让您对"归零"的情况持开放态度.(即-init返回,对象指针变得对另一个线程可见,其他线程尝试访问该令牌,但它仍然是垃圾,因为还没有发生归零操作.)为了避免这个问题,OSMemoryBarrier()到你的-init方法结束,你应该没问题.(请注意,在这里添加内存屏障以及内存屏障一般会有非零性能损失.)内存屏障细节留作 "进一步阅读"(但如果你要依赖它们,你最好先了解它们,至少在概念上.)

我的猜测是,dispatch_once与非全局/静态存储一起使用的"禁止" 源于无序执行和内存障碍是复杂的主题这一事实,正确的障碍是困难的,弄错它往往导致极其微妙也许最重要的是(虽然我没有根据经验测量),引入所需的内存屏障以确保dispatch_once_t在ivar中的安全使用几乎肯定会否定一些(所有?)性能dispatch_once超过"经典"锁定模式的好处.

另请注意,有两种"重新排序".重新排序是作为编译器优化发生的(这是由volatile关键字影响的重新排序),然后在不同的体系结构上以不同的方式在硬件级别重新排序.这种硬件级重新排序是由内存屏障操纵/控制的重新排序.(即volatile关键字不够用.)

OP特别询问了"完成一次"的方法.在ReactiveCocoa的RACDisposable类中可以看到这样一个模式的一个例子(对我来说看起来是安全/正确的),它在处理时保持零个或一个块运行并保证"一次性"只被处置一次,并且块(如果有的话)只被调用一次.它看起来像这样:

@interface RACDisposable ()
{
        void * volatile _disposeBlock;
}
@end

...

@implementation RACDisposable

// <snip>

- (id)init {
        self = [super init];
        if (self == nil) return nil;

        _disposeBlock = (__bridge void *)self;
        OSMemoryBarrier();

        return self;
}

// <snip>

- (void)dispose {
        void (^disposeBlock)(void) = NULL;

        while (YES) {
                void *blockPtr = _disposeBlock;
                if (OSAtomicCompareAndSwapPtrBarrier(blockPtr, NULL, &_disposeBlock)) {
                        if (blockPtr != (__bridge void *)self) {
                                disposeBlock = CFBridgingRelease(blockPtr);
                        }

                        break;
                }
        }

        if (disposeBlock != nil) disposeBlock();
}

// <snip>

@end
Run Code Online (Sandbox Code Playgroud)

OSMemoryBarrier()在init中使用,就像你必须使用的一样dispatch_once,然后使用OSAtomicCompareAndSwapPtrBarrier它,顾名思义,暗示一个内存屏障,以原子方式"翻转开关".如果不清楚的话,这里发生的事情是-initivar设定的时间self.这个条件被用作"标记",以区分" 没有阻止但我们没有处置 "和" 有一个阻止但我们已经处置 "的情况.

实际上,如果内存障碍对你来说看起来不透明和神秘,我的建议就是使用经典的锁定模式,直到你测量出那些经典的锁定模式导致应用程序出现真正的,可测量的性能问题.


CRD*_*CRD 5

艾弗纳,你现在可能会后悔当然;-)

关于你对问题的编辑,并考虑到其他问题,你或多或少地重建了"老派"这样做的方式,也许这就是你应该做的事情(代码直接输入,期待拼写错误):

@implemention RACDisposable
{
   BOOL ranExactlyOnceMethod;
}

- (id) init
{
   ...
   ranExactlyOnceMethod = NO;
   ...
}

- (void) runExactlyOnceMethod
{
   @synchronized(self)     // lock
   {
      if (!ranExactlyOnceMethod) // not run yet?
      {
          // do stuff once
          ranExactlyOnceMethod = YES;
      }
   }
}
Run Code Online (Sandbox Code Playgroud)

对此有一个共同的优化,但考虑到其他讨论,让我们跳过它.

这"便宜"吗?可能不是,但所有的东西都是相对的,它的费用可能并不重要 - 但是YMMV!

HTH

  • 在这里使用递归锁意味着如果"do stuff once"中的代码意外地重新调用-runExactlyOnceMethod,则可能违反"一次运行"不变量 (4认同)