2013-11-07 2 views
10

Есть несколько источников, требующие конкретный метод, но я хотел бы убедиться, что он вызывается ровно один раз (за объект)Использование 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

См [Могу ли я объявить dispatch_once_t предикат в качестве переменной-члена вместо статические] (http://stackoverflow.com/questions/13856037/can-i-declare-dispatch-once-t-predicate- как-член-переменной вместо-о-статического). – CRD

+0

Итак, вопрос в том, что это лучший способ сделать это –

ответ

5

Авнер, вы, вероятно, сожалея вы спросили сейчас ;-)

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

@implemention RACDisposable 
{ 
    BOOL ranExactlyOnceMethod; 
} 

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

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

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

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

НТН

+4

Использование рекурсивного замка здесь означает, что может быть нарушен инвариант «побежал ровно один раз», если код в «делать что-то один раз» случайно повторяется - runExactlyOnceMethod –

+1

@Catfish_Man - Если я правильно понимаю вашу проблему, вы защитите себя от это, установив 'ranExactlyOnceMethod' в' YES' в качестве первой инструкции в 'if'? (Также, безусловно, нерекурсивная блокировка приведет к тупиковой ситуации в вашем сценарии.) – CRD

+0

Да, вы можете это сделать. Существует три режима отказа: тупик, многократное выполнение или не дождаться завершения запуска. Каждый компромисс может быть уместным, но dispatch_once выбирает блокировку, поэтому, если пытаться имитировать его, это наиболее совместимое поведение. –

-3

dispatch_once() выполняет свой блок один раз и только один раз в течение срока службы приложения. Вот GCD reference link. Так как вы говорите, что вы хотите случаться один раз для каждого объекта, вы не должны использовать dispatch_once()

9

Как уже упоминались в linked answer to a similar question, справочная документация говорит:

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

Общая озабоченность хорошо указана в этом ответе. Тем не менее, можно заставить его работать. Чтобы уточнить: проблема заключается в том, что хранилище для предиката должно быть надежно обнулено при инициализации. С помощью статической/глобальной семантики это гарантируется. Теперь я знаю, о чем вы думаете, «... но объекты Objective-C также обнуляются на init!», И вы были бы в целом правы. Там, где проблема возникает, это переупорядочение чтения/записи. Некоторые архитектуры (например, ARM) имеют модели памяти weakly consistent, что означает, что считывание/запись в память может быть переупорядочено до тех пор, пока сохраняется первоначальное намерение основного потока последовательности выполнения. В этом случае переупорядочение может потенциально оставить вас в ситуации, когда операция «обнуления» задерживается, так что после после другой поток пытается прочитать токен. (т.е. -init возвращает, указатель объекта становится видимым для другого потока, который другой поток пытается получить доступ к токену, но он все еще мусор, потому что операция обнуления еще не была выполнена.) Чтобы избежать этой проблемы, вы можете добавить вызов OSMemoryBarrier() до конца вашего метода -init, и все должно быть в порядке. (Обратите внимание, что существует ненулевое ограничение производительности для добавления барьера памяти здесь и к барьерам памяти в целом.) details of memory barriers оставлены как «дальнейшее чтение» (но если вы собираетесь полагаться на них, рекомендуется понимать их, по крайней мере, концептуально.)

Моя догадка заключается в том, что «запрет» на использование dispatch_once с неглобальным/статическим хранилищем связан с тем, что нестандартные исполнения и барьеры памяти сложны темы, преодоление препятствий, трудно, их неправильное поведение приводит к чрезвычайно тонким и труднодоступным ошибкам и, возможно, самое важное (хотя я не измерял его эмпирически), вводя необходимый барьер памяти для обеспечения безопасности использование dispatch_once_t в ivar почти наверняка отрицает некоторые (все?) преимущества в производительности, которые dispatch_once имеет ov er "классические" блокировки.

Также обратите внимание, что существует два типа «переупорядочения».«Там происходит переупорядочение, которое происходит как оптимизация компилятора (это переупорядочение, которое выполняется с помощью ключевого слова volatile), а затем происходит переупорядочение на аппаратном уровне по-разному на разных архитектурах. Этот перезарядка аппаратного уровня это переупорядочение, которое управляется/управляется барьером памяти (например, ключевое слово volatile не является достаточным.)

ОП спрашивал конкретно о способе «закончить один раз». Один пример (что для моих глаз появляется безопасный/правильный) для такого шаблона можно увидеть в классе 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 

Он использует в INIT OSMemoryBarrier(), так же, как вы бы использовать для dispatch_once, то он использует OSAtomicCompareAndSwapPtrBarrier, который, как следует из названия, предполагает барьер памяти, атомарный «щелкнуть выключатель». В случае, если неясно, что здесь происходит, то, что на -init время ivar установлено на self. Это условие используются в качестве «маркеров», чтобы различать между случаями «нет блока, но мы не склонны» и «был блоком, но мы уже располагали.»

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

+0

Связанный ответ на самом деле предполагает, что это нормально использовать переменные экземпляра. Ваши опасения по поводу слабо согласованной модели памяти, похоже, будут неуместны по отношению к переменным экземпляра. ARC в основном полагается на переменные экземпляра, начиная с нуля. Если есть проблема с моделью памяти, то результат «alloc» потенциально видим для других потоков перед обнулением, конечно, Apple включит необходимый барьер памяти во время выполнения? – CRD

+0

ARC здесь не имеет значения. 'dispatch_once_t' - это примитивный тип, а не тип подсчета. ARC никогда не будет читать/писать в это место, поэтому он не будет влиять на упорядочение записи в его местоположение. – ipmcc

+1

Кроме того, я могу * уверен, что в 'alloc' нет барьера памяти, поскольку производительность будет * abysmal *. – ipmcc

Смежные вопросы