I am trying to implement an operation queue and I have the following scenario:
NSOperation A
NSOperation B
NSOperation C
NSOperation D
NSOperationQueue queue
So basically you just need to make sure the first one finishes before beginning the next? NSOperationQueue will run in parallel unless you tell it not to. You can call setMaxConcurrentOperationCount: on your operation queue and set it to one to basically turn it into a serial queue in which only one operation will run at a time.
I think that you are following a wrong approach.If every operation in the queue has a priority, and they must be executed in order, why not using 4 different threads?
Take an ivar that represents the state (0: no operation is completed, 1: one operation is completed, and so on), protect it with a condition:
@property(nonatomic,strong) NSCondition* condition;
@property (nonatomic) NSUInteger state;
Initalize everything (state starts with zero), then create 4 different threads with different priorities.This is an example for the selector executed by the thread A:
- (void) threadA : (id) sender
{
[condition lock];
while(state!=3)
{
[condition wait];
}
// Do the job here
state=4; // That's kinda useless but useful if in future you
// want another thread that starts doing the job when A ends
[condition unlock];
}
So all gets executed in the order that you want.
EDIT
You can do the equivalent that I did here, but using a NSOperationQueue:
NSOperationQueue* queue=[NSOperationQueue new];
[queue setMaxConcurrentOperationCount: 4];
[queue addOperation: [[NSInvocationOperation alloc]initWithTarget: self selector: @selector(threadA:) object: nil]]
By saying that you are following the wrong approach I mean that you shouldn't use a queue with 1 as maxConcurrentOperationCount. The main queue has this value set to 1 and that's the reason of your troubles.
As you've found, you can't really do this with dependencies because that only affects when an operation starts - and if you don't know you'll need the sub operations until the main one is running, so that's no good. You can't solve this problem with a single operation queue.
However, since you're already running on an operation queue, there is no need to add the further operations to a queue. Just execute them synchronously in-place. You have to wait for them to return anyway, so why not?
Once an NSOperation is in its main method you have to go through with it. There is no paused state, only finished or cancelled.
I would implement a NSCopying on operation A which would copy the entire state into a new instance. You would have a delegate method or block which is able to communicate that this operation cannot go through because it is missing info from operation B.
So the process would go such:
Inside the delegate you have to make sure to suspend the queue to avoid a race condition. After the above steps you resume the queue. In operation A you would have multiple places where you check for isCancelled to actually not do any more work in main when it has been cancelled.
Here's two ideas for you with contrived examples. I only used two operations but you could expand the concept to any number and/or nest them as needed.
GCD provides lightweight "dispatch groups", which allow you to explicitly order tasks and then wait on their completion. In this case AlphaOperation creates a group and enters it, then starts BetaOperation, whose completionBlock
causes the group to be left. When you call dispatch_group_wait
, the current thread blocks until the number of times entering the group is equal to the number of times leaving it (a lot like retain count). Don't forget to check the isCancelled
state of the operation after any potentially long-running task.
@interface BetaOperation : NSOperation
@end
@implementation BetaOperation
- (void)main
{
NSLog(@"beta operation finishing");
}
@end
@interface AlphaOperation : NSOperation
@end
@implementation AlphaOperation
- (void)main
{
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
BetaOperation *betaOperation = [[BetaOperation alloc] init];
betaOperation.completionBlock = ^{
dispatch_group_leave(group);
};
[betaOperation start];
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
if ([self isCancelled])
return;
NSLog(@"alpha operation finishing");
}
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
dispatch_async(dispatch_get_main_queue(), ^{
AlphaOperation *operation = [[AlphaOperation alloc] init];
[operation start];
});
return YES;
}
@end
Since you're already with working operations, another option is creating a queue as a property of AlphaOperation, then adding BetaOperation and calling waitUntilAllOperationsAreFinished
on the queue. This has an added benefit in that you can easily cancel the queue's operations when AlphaOperation is cancelled, simply by overriding the cancel
method.
@interface BetaOperation : NSOperation
@end
@implementation BetaOperation
- (void)main
{
NSLog(@"beta operation finishing");
}
@end
@interface AlphaOperation : NSOperation
@property (strong) NSOperationQueue *queue;
@end
@implementation AlphaOperation
- (void)main
{
self.queue = [[NSOperationQueue alloc] init];
BetaOperation *betaOperation = [[BetaOperation alloc] init];
[self.queue addOperation:betaOperation];
[self.queue waitUntilAllOperationsAreFinished];
if ([self isCancelled])
return;
NSLog(@"alpha operation finishing");
}
- (void)cancel
{
[super cancel];
[self.queue cancelAllOperations];
}
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
dispatch_async(dispatch_get_main_queue(), ^{
AlphaOperation *operation = [[AlphaOperation alloc] init];
[operation start];
});
return YES;
}
@end
Try using setCompletionBlock:
like this:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSOperation *operationA;
NSOperation *operationB;
//... initialize operationA and operationB however you please ...
[operationA setCompletionBlock:^{
if ([operationA satisfiesSomeCriteria]) {
[queue addOperation:operationB];
}
}];
[queue addOperation:operationA];
When you set a completion block on an operation, it is executed after the main task of the operation is completed or cancelled. Thus the results of the work the operation was executing are available so that you can decide whether the next operation should be added to the queue.