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

时间:2013-11-07 09:25:41

标签: objective-c singleton 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{...}

问题是令牌是否在同一个类的所有实例中共享 - 所以不是一个好的解决方案 - 每个对象是否有一个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
}

3 个答案:

答案 0 :(得分:9)

linked answer to a similar question中所述,参考文档说:

  

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

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

我的猜测是,将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

它在初始化中使用OSMemoryBarrier(),就像你必须用于dispatch_once一样,然后它使用OSAtomicCompareAndSwapPtrBarrier,正如其名称暗示的那样,意味着一个内存屏障,原子地“调节开关”。如果不清楚,这里发生的是在-init时,ivar设置为self。这个条件被用作“标记”来区分“没有阻止但我们没有处理”的情况和“有一个阻止,但我们已经处理了。” EM>“

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

答案 1 :(得分:5)

Avner,你可能会后悔当时被问到这个问题; - )

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

@implemention RACDisposable
{
   BOOL ranExactlyOnceMethod;
}

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

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

对此有一个共同的优化,但鉴于其他讨论,我们跳过它。

这“便宜”吗?可能不是,但所有事情都是相对的,它的费用可能并不重要 - 但是YMMV!

HTH

答案 2 :(得分:-3)

dispatch_once()执行其块一次,仅在应用程序的生命周期内执行一次。这是GCD reference link。由于您提到您希望每个对象发生一次[self _finishOnce],因此您不应该使用dispatch_once()