Использование dispatch_once_t для каждого объекта, а не класса

Существует несколько источников, вызывающих определенный метод, но я хотел бы убедиться, что он вызывается ровно один раз (на объект)

Я хотел бы использовать синтаксис вроде

// 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 для объекта - если нет, то лучший способ гарантировать, что он вызывается один раз?

EDIT:

Вот предлагаемое решение, о котором я думаю, - похоже ли это хорошо?

@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
}

Ответ 1

Авнер, вы, вероятно, сожалеете, что уже задали вопрос: -)

Что касается вашего редактирования вопроса и с учетом других проблем, вы более или менее воссоздали способ "старой школы", и, возможно, это именно то, что вы должны делать (код, набранный напрямую, ожидайте опечатки):

@implemention RACDisposable
{
   BOOL ranExactlyOnceMethod;
}

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

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

Существует общая оптимизация, но, учитывая другое обсуждение, пропустите это.

Является ли это "дешевым"? Ну, вероятно, нет, но все вещи относительны, его расход, вероятно, невелик - но YMMV!

НТН

Ответ 2

Как упоминалось в связанном ответе на аналогичный вопрос, в справочной документации говорится:

Предикат должен указывать на переменную, хранящуюся в глобальной или статической объем. Результат использования предиката с автоматическим или динамическим хранилище undefined.

Общие проблемы хорошо перечислены в этом ответе. Тем не менее, можно заставить его работать. Чтобы уточнить: проблема заключается в том, что хранилище для предиката должно быть надежно обнулено при инициализации. С помощью статической/глобальной семантики это гарантируется. Теперь я знаю, что вы думаете: "... но объекты 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, как и вам нужно использовать для dispatch_once, тогда он использует OSAtomicCompareAndSwapPtrBarrier, который, как следует из названия, подразумевает барьер памяти, чтобы атомизировать "перевернуть переключатель". Если неясно, что здесь происходит, то при -init время ivar устанавливается на self. Это условие используется как "маркер", чтобы различать случаи "нет блока, но мы не располагаем" и "был блок, но мы уже располагались".

В практическом плане, если барьеры памяти кажутся вам непрозрачными и загадочными, мой совет будет состоять в том, чтобы просто использовать классические блокировки, пока вы не заметите, что эти классические блоки блокировки вызывают реальные и измеримые проблемы с производительностью для вашего приложения.

Ответ 3

dispatch_once() выполняет свой блок один раз и только один раз для срока службы приложения. Здесь ссылка ссылка GCD. Поскольку вы упоминаете, что хотите, чтобы [self _finishOnce] выполнялся один раз на объект, вы не должны использовать dispatch_once()