10

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

我想使用类似的语法

// 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
}
4

3 回答 3

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

OSMemoryBarrier()在 init 中使用,就像您必须使用 for 一样dispatch_once,然后它使用OSAtomicCompareAndSwapPtrBarrier它,顾名思义,它意味着内存屏障,以原子地“翻转开关”。如果不清楚,这里发生的事情是-initivar 设置为self. 此条件用作“标记”,以区分“没有块但我们尚未处理”和“有块但我们已经处理”的情况。

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

于 2013-11-07T12:00:49.827 回答
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!

高温高压

于 2013-11-08T01:52:01.060 回答
-3

dispatch_once()在应用程序的生命周期内执行其块一次且仅一次。这是GCD 参考链接。既然你提到你希望[self _finishOnce]每个对象发生一次,你不应该使用dispatch_once()

于 2013-11-07T09:49:12.987 回答