Использование 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()