I have just finished moving a large part of production code to use NSOperation and NSOperationQueues. The typical solutions to share results (notifications, delegates) seemed cumbersome and unwieldy so here is my solution.
Subclass NSOperationQueue to include a thread safe mutable dictionary instance property. I adapted the thread safe mutable dictionary code published here into my subclass JobOperationQueue: https://www.guiguan.net/ggmutabledictionary-thread-safe-nsmutabledictionary/
Subclass NSOperation to include a reference back to it's owning/initial JobOperationQueue. This ensures the operation can always find it's owner, even when code has to get run on different queues (which happens more than I thought!). Add subclass methods to JobOperationQueue to set this value whenever an operation is added to the queue via addOperation: or addOperations:
As operations process, they can add values into the queue's dictionary, and access values placed there by earlier processes.
I have been very happy with this approach and it has solved a lot of issues.
Be careful regarding race conditions -- if one operation MUST HAVE a value from another operation, ensure there is a dependency explicitly added to ensure the order of operations.
Here are my two main classes. I also added two-way dependency information, which I found useful in the situation where an operation has to spawn child operations, but wants to maintain the dependency network. In that situation you have to know who is depending on the original operation, so that you can propagate dependencies onto the spawned operations.
//
// JobOperation.h
//
//
// Created by Terry Grossman on 9/17/15.
//
#import <Foundation/Foundation.h>
#import "JobOperationQueue.h"
#import "ThreadSafeMutableDictionary.h"
#import "ThreadSafeMutableArray.h"
@interface JobOperation : NSOperation
@property (strong, atomic) ThreadSafeMutableArray *dependents;
@property (strong, atomic) NSDate *enqueueDate;
@property (weak, atomic) JobOperationQueue *homeJobQueue;
-(ThreadSafeMutableDictionary *)getJobDict;
@end
//
// JobOperation.m
//
//
// Created by Terry Grossman on 9/17/15.
//
#import "JobOperation.h"
@implementation JobOperation
- (id)init
{
if((self = [super init])) {
_dependents = [[ThreadSafeMutableArray alloc] init];
}
return self;
}
-(ThreadSafeMutableDictionary *)getJobDict
{
id owningQueue = self.homeJobQueue;
if (owningQueue && [owningQueue isKindOfClass:[JobOperationQueue class]])
{
return ((JobOperationQueue *)owningQueue).jobDictionary;
}
// try to be robust -- handle weird situations
owningQueue = [NSOperationQueue currentQueue];
if (owningQueue && [owningQueue isKindOfClass:[JobOperationQueue class]])
{
return ((JobOperationQueue *)owningQueue).jobDictionary;
}
return nil;
}
-(void) addDependency:(NSOperation *)op
{
[super addDependency:op]; // this adds op into our list of dependencies
if ([op isKindOfClass:[JobOperation class]])
{
[((JobOperation *)op).dependents addObject:self]; // let the other job op know we are depending on them
}
}
@end
//
// JobOperationQueue.h
//
//
// Created by Terry Grossman on 9/17/15.
//
#import <Foundation/Foundation.h>
#import "ThreadSafeMutableDictionary.h"
// A subclass of NSOperationQueue
// Adds a thread-safe dictionary that queue operations can read/write
// in order to share operation results with other operations.
@interface JobOperationQueue : NSOperationQueue
// If data needs to be passed to or between job operations
@property (strong, atomic) ThreadSafeMutableDictionary *jobDictionary;
-(void)addOperation:(NSOperation *)operation;
-(void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait;
+(BOOL) checkQueue:(JobOperationQueue *)queue hasOpsOlderThan:(NSInteger)secondsThreshold cancelStaleOps:(BOOL)cancelOps;
@end
//
// JobOperationQueue.m
//
//
// Created by Terry Grossman on 9/17/15.
//
#import "JobOperationQueue.h"
#import "JobOperation.h"
@implementation JobOperationQueue
// if this method returns NO, should set the queue to nil and alloc a new one
+(BOOL) checkQueue:(JobOperationQueue *)queue hasOpsOlderThan:(NSInteger)secondsThreshold cancelStaleOps:(BOOL)cancelOps
{
if (queue == nil)
{
return NO;
}
if ([queue operationCount] > 0)
{
NSLog(@"previous share still processing!");
// recently started or stale? Check the enqueue date of the first op.
JobOperation *oldOp = [[queue operations] objectAtIndex:0];
NSTimeInterval sourceSeconds = [[NSDate date] timeIntervalSinceReferenceDate];
NSTimeInterval destinationSeconds = [oldOp.enqueueDate timeIntervalSinceReferenceDate];
double diff = fabs( destinationSeconds - sourceSeconds );
if (diff > secondsThreshold)
{
// more than three minutes old! Let's cancel them and tell caller to proceed
[queue cancelAllOperations];
return NO;
}
else
{
return YES;
}
}
return NO;
}
-(id) init;
{
if((self = [super init])) {
_jobDictionary = [[ThreadSafeMutableDictionary alloc] initWithCapacity:12];
}
return self;
}
-(void)addOperation:(NSOperation *)operation;
{
if (operation == nil)
{
return;
}
if ([operation isKindOfClass:[JobOperation class]])
{
((JobOperation *)operation).enqueueDate = [NSDate date];
//((JobOperation *)operation).homeQueueT = self.underlyingQueue; // dispatch_get_current_queue();
((JobOperation *)operation).homeJobQueue = self;
}
[super addOperation:operation];
}
-(void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait;
{
for (NSOperation *operation in ops)
{
if ([operation isKindOfClass:[JobOperation class]])
{
((JobOperation *)operation).enqueueDate = [NSDate date];
//((JobOperation *)operation).homeQueueT = self.underlyingQueue; //dispatch_get_current_queue();
((JobOperation *)operation).homeJobQueue = self;
}
}
[super addOperations:ops waitUntilFinished:wait];
}
@end