Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
348 views
in Technique[技术] by (71.8m points)

objective c - Using dispatch_once_t per object and not per class

There are multiple sources calling a particular method, but I would like to ensure that it is called exactly once (per object)

I would like to use syntax like

// 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{...}

Problem is the token is shared accross all instances of the same class - so not a good solution - is there a dispatch_once_t per object - if not what is the best way to ensure it is called once?

EDIT:

Here is a proposed solution I am thinking of - does it seem alright?

@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
}
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

As mentioned in the linked answer to a similar question, the reference documentation says:

The predicate must point to a variable stored in global or static scope. The result of using a predicate with automatic or dynamic storage is undefined.

The overall concerns are well enumerated in that answer. That said, it is possible to make it work. To elaborate: The concern here is that the storage for the predicate be reliably zero'ed out on initialization. With static/global semantics, this is strongly guaranteed. Now I know what you're thinking, "...but Objective-C objects are also zeroed out on init!", and you'd be generally right. Where the problem comes in is with read/write re-ordering. Certain architectures (i.e. ARM), have weakly consistent memory models, which means that memory reads/writes can be re-ordered as long as the original intent of the primary thread of execution's consistency is preserved. In this case, re-ordering could potentially leave you open to a situation where the "zeroing" operation is delayed such that it happened after another thread tries to read the token. (i.e. -init returns, the object pointer becomes visible to another thread, that other thread tries to access the token, but it is still garbage because the zeroing operation has not happened yet.) To avoid this problem, you can add a call to OSMemoryBarrier() to the end of your -init method, and you should be OK. (Note that there is a non-zero performance penalty to adding a memory barrier here, and to memory barriers in general.) The details of memory barriers are left as "further reading" (but if you're going to rely on them, you'd be well advised to understand them, at least conceptually.)

My guess is that the "prohibition" on using dispatch_once with non-global/static storage stems from the fact that out-of-order execution and memory barriers are complex topics, getting barriers right is hard, getting them wrong tends to lead to extremely subtle and hard-to-nail-down bugs and, perhaps most importantly (although I haven't measured it empirically), introducing the required memory barrier to ensure safe use of the dispatch_once_t in an ivar almost certainly negates some (all?) of the performance benefit that dispatch_once has over "classic" locking patterns.

Also note that there are two kinds of "re-ordering." There's re-ordering that happens as a compiler optimization (this is the re-ordering that is effected by the volatile keyword) and then there's re-ordering at the hardware level in different ways on different architectures. This hardware-level re-ordering is the re-ordering that is manipulated/controlled by a memory barrier. (i.e. the volatile keyword is not sufficient.)

OP was asking specifically about a way to "finish once." One example (that to my eyes appears safe/correct) for such a pattern can be seen in ReactiveCocoa's RACDisposable class, which keeps zero or one blocks to run at disposal time and guarantees that the "disposable" is only ever disposed once, and that the block, if there is one, is only ever called once. It looks like this:

@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

It uses OSMemoryBarrier() in init, just like you would have to use for dispatch_once, then it uses OSAtomicCompareAndSwapPtrBarrier which, as the name suggests, implies a memory barrier, to atomically "flip the switch". In case it's not clear, what's going on here is that at -init time the ivar is set to self. This condition is used as a "marker" to differentiate between the cases of "there is no block but we have not disposed" and "there was a block but we have already disposed."

In practical terms, if memory barriers seem opaque and mysterious to you, my advice would be to just use classic locking patterns until you've measured that those classic locking patterns are causing real, measurable performance issues for your application.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...