I have extended NSOperationQueue
to allow adding NSBlockOperation
with a specific NSString
as identifier.
The identifier value is held in a NSMutableArray
serving as a registry. This is how I implement the registry.
-(void)addOperation:(NSOperation *)operation withID:(NSString*)operationID
{
@synchronized(self.queueReference)
{
[self.queueReference addObject:operationID]; // <-- just a mutable array
}
[operation setCompletionBlock:^(){
@synchronized(self.queueReference) {
[self.queueReference removeObject:operationID];
}
}];
[self addOperation:operation];
}
Basically I am adding a completion block which is cleaning the registry when that particular operation has finished.
However, while this works, I am in need to add more granularity to the queue.
I only use the queue with block operation, and during the execution of the block I may send different NSNotification
to the listener depending how the execution went.
What I was trying to achieve:
A caller try to add a particular NSBlockOperation
with identifier to queue. If queue already has such identifier just don't add block, and the calling class set itself as listener.
What is missing ? Checking for the identifier is not enough, there may be case when the NSBlockOperation
already dispatched the NSNotification
but the completion block has not yet being called.
So the caller class ask the queue, which is saying the identifier exists in registry, and caller wrongly set itself for listening to a notification that will never arrive because it's already being sent.
The scenario would be instead: caller ask the queue, which is saying 'identifier is in registry' but NSNotification
is sent. And the caller put NSBlockOperation
to queue.
The check of registry is made by means of a simple method:
-(BOOL)hasOperationWithID:(NSString*)operationID
{
@synchronized(self.queueReference)
{
return [self.queueReference containsObject:operationID];
}
}
but at this point I have not much idea on how to extend such method. The code I am working on is kind of 'academic', it does not serve any particular purpose, it is just me trying to experiment. Therefore I have great flexibility within the code. But this is quite new subject to me, so please be as much specific as possible of any downside of suggested implementation.
It looks like your current system has three fundamental events:
Unless the queue itself explicitly listens for any NSNotifications
that might be sent by the blocks, it has no way of knowing whether they have happened yet. But even if it does listen, the ordering in which observers of NSNotifications
are called is non-deterministic. In other words, even if the queue listens for the notification and interlocks its callback with enqueue/dequeue operations, it could (and eventually would) still be too late for another client to start listening for that NSNotification
, and you would falsely reject an operation.
Consider this alternative: Instead of using the completion block to manage the identifier list, use the notification itself -- have the queue handle sending the notifications. Put differently, let's get rid of the third event and have the notification sending do double duty for identifier list maintenance. The simplest way I came up with to do this looked like:
Header:
//
// SONotifyingOperationQueue.h
// NotifyingOpQueue
//
typedef void (^SOSendNotificationBlock)(NSDictionary* userInfo);
typedef void (^SONotifyingBlock)(SOSendNotificationBlock sendNotificationBlock);
@interface SONotifyingOperationQueue : NSOperationQueue
- (BOOL)addOperationForBlock:(SONotifyingBlock)block withNotificationName:(NSString*)notificationName;
@end
Implementation
//
// SONotifyingOperationQueue.m
// NotifyingOpQueue
//
#import "SONotifyingOperationQueue.h"
@implementation SONotifyingOperationQueue
{
NSMutableSet* _names;
}
- (BOOL)addOperationForBlock: (SONotifyingBlock)block withNotificationName: (NSString*)notificationName
{
notificationName = [[notificationName copy] autorelease];
BOOL shouldAdd = NO;
@synchronized(self)
{
_names = _names ? : [[NSMutableSet alloc] init];
if (![_names containsObject: notificationName])
{
[_names addObject: notificationName];
shouldAdd = YES;
}
}
if (shouldAdd)
{
NSBlockOperation* blockOp = [[[NSBlockOperation alloc] init] autorelease];
__block SONotifyingOperationQueue* blockSelf = self;
SOSendNotificationBlock notificationBlock = ^(NSDictionary* userInfo){
@synchronized(blockSelf)
{
[blockSelf->_names removeObject: notificationName];
// Sending the notification from inside the @synchronized makes it atomic
// with respect to enqueue operations, meaning there can never be a missed
// notification that could have been received.
[[NSNotificationCenter defaultCenter] postNotificationName: notificationName object: blockSelf userInfo: userInfo];
}
};
dispatch_block_t executionBlock = ^{
block(notificationBlock);
};
[blockOp addExecutionBlock: executionBlock];
[self addOperation: blockOp];
}
return shouldAdd;
}
- (void)dealloc
{
[_names release];
[super dealloc];
}
@end
This approach makes several changes to your original approach. First, the API here adds blocks and not NSOperations
. You could do the same thing with an NSOperation
subclass, but it would be more code, and wouldn't change the overall pattern. It also merges the notion of the identifier and the notification name. If an operation could send multiple, different NSNotifications
, this won't work without modification, but again, the overall pattern would be the same. The important feature of this pattern is that your id/name check is now interlocked with the notification sending itself, providing a strong guarantee that if someone goes to add a new block/operation to the queue, and another operation with the same id/name hasn't fired its notification yet, the new operation won't be added, but if the notification has been fired, then it will be added, even if the preceding block hasn't yet completed.
If having the NSOperation
object was somehow important here, you could also have the method here return the operation it creates for the supplied block.
HTH.