NSOperation - Forcing an operation to wait others dynamically
Asked Answered
A

7

28

I am trying to implement an operation queue and I have the following scenario:

NSOperation A
NSOperation B
NSOperation C
NSOperation D
NSOperationQueue queue

I start adding A to queue.

During the execution of A I need to get some data from B and I can't continue with A until B returns what I need.

The same situation will occur for B depending on C and for C depending on D.

To manage this, at each NSOperation I have this code:

NSOperation *operation; //This can be A, B, C, D or any other NSOperation

[self setQueuePriority:NSOperationQueuePriorityVeryLow]; //Set the current NSOperation with low priority

[queue addOperation: operation]; //Add the operation that I want to the queue

while(!operation.isFinished && !self.isCancelled){} //I need to wait the operation that I depend before moving on with the current operation

[self setQueuePriority:NSOperationQueuePriorityNormal]; //After the while, the other operation finished so I return my priority to normal and continue

if(self.isCancelled){ //If I get out of the while because the current operation was cancelled I also cancel the other operation.
[operation cancel];          
}

My problem is that when I have something like 3 or 4 NSOperations waiting and executing the while(!operacao.isFinished && !self.isCancelled){} my code just freeze because the NSOperation that is important to me don't get executed, even if it have higher priority.

What I tried

  • Adding dependency during execution time but since my NSOperation is already running I doesn't seems to have any effect.

  • Instead of adding the operation to queue, I can do something [operation start]. It works, but canceling the current operation will also cancel the other operations that I started?

  • I can do something like while(!operacao.isFinished && !self.isCancelled){[NSThread sleepForTimeInterval:0.001];}. It works, but is this the correct way? Maybe there is a better solution.

In this situation how I can guarantee that the operation that I want will run and the others will wait in background? What is the correct way to solve this?

If anyone question me why I don't add the dependency before starting my queue its because an operation will need the other only if some conditions are true. I will know if I need other operation only during execution time.

Thanks for your time.

Allyn answered 7/12, 2012 at 11:43 Comment(0)
T
37

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.

Example 1: Using Grand Central Dispatch

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

Example 2: Using a local NSOperationQueue

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
Tactile answered 18/12, 2012 at 21:42 Comment(1)
Thank you. The local NSOperationQueue worked and solved my problem. I created a super class, so each NSOperation that I use will extend this class and have it own local queue. With this I can add operations dynamically and have a wait state for other threads. I will check in the future if having to many queues can be a problem, but until now everything its running ok. Thank you and thanks for all the others who looked my questions and helped me with suggestions.Allyn
K
6

One approach is to manage this from outside the operation classes ie. setup the operation dependencies between A/B/C/D correctly while creating them.

Steps: (In the method that is creating these operations)

1) Create Operation A

2) If data provided by Operation B is not available, create Operation B, and make Operation A dependent on Operation B. ie. something like operationA.addDependency(operationB);

3). repeat step 2 for C and D. (ie. B depends on C and C depends on D, if required)

4) Add the operations to queue. The queue will execute based on the dependencies ie. D, C, B, A.

Kiblah answered 14/12, 2012 at 8:2 Comment(1)
Hi, thanks for your reply. Using this I think I will need to check conditions before starting my operations and I need to try to find a way to solve this at execution time. If there is no way I will change my program to something like you suggested. Thanks.Allyn
P
3

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.

Paoting answered 17/12, 2012 at 1:3 Comment(1)
Hi, thanks for your reply. Using this I think I will need to check conditions before starting my operations and I need to try to find a way to solve this at execution time. If there is no way I will change my program to something like you suggested. Thanks.Allyn
S
2

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.

Semiporcelain answered 7/12, 2012 at 11:54 Comment(5)
Thanks for your reply. I already have one operation running, if my maxConcurrentOperationCount is 1 and I add a new operation it will execute?Allyn
maxConcurrentOperationCount just means it will only run one at any given time. As soon as one finishes, the next one will begin.Semiporcelain
So I think that this will not help in my case. My Operation A is running, I need to run Operation B and when B finish, continue running A. In my scenario I can have multiple operations running.Allyn
NSOperationQueue may not be for you then. It's more designed to run a series of independent tasks. You might checkout +sendAsynchronousRequest:queue:completionHandler: in NSURLConnection and start the next task in the completion handlerSemiporcelain
I will check the sendAsynchronousRequest:queue:completionHandler: to see if it solve my problem. Thanks for your time.Allyn
R
2

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:

  • Create Operation A, set delegate
  • you cannot proceed, delegate method fires
  • the delegate creates a new operation B, creates a copy of operation A, sets the dependency such that A will wait for B's completion
  • then the delegate cancels the original op A

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.

Repine answered 16/12, 2012 at 8:32 Comment(1)
Hi, thanks for your reply. I got some questions about your suggestion. If I use this way, when I cancel the original op A it will start again when I call it after B completion? The B operation will run in a different queue?Allyn
B
2

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.

Barathea answered 18/12, 2012 at 0:58 Comment(6)
Hi, thanks for your answer. So I will use NSThread instead of NSOperation? If in the future I need to use an queue similiar to NSOperationQueue, will be possible to do that with NSThreads? Or i will have to implement my own?Allyn
A NSOperationQueue is just a class that handles operations for you.By using NSThread's I do the same that you do with NSOperationQueue, but manually, I'll explain you more after I'll edit the answer.Barathea
In my case I dont have maxConcurrentOperationCount set as 1, the value is limited to the device support. The reason of my trouble is that with NSOperation I dont have a "wait" command. When 4 operations are running in parallel the while(){DoNothing} takes too much processing time, even if the operation priority is changed. Putting the NSOperation to sleep in my while loop solve the problem. But maybe there is a better way to implement a wait state?Allyn
Yes, the code works and the [condition wait] its a possible solution. But I used the local queue to write less code and let the ios take care of these details for me. Anyway, thank you very much for your time and for your help.Allyn
No problem, just a tip: if you used the local queue you may check the maxConcurrentOperationCount and set it to 5 in order to make this code work.Barathea
Thanks, my maxConcurrentOperationCount its "unlimited". My limit has the same value as the constant NSOperationQueueDefaultMaxConcurrentOperationCount.Allyn
S
1

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?

Starlastarlene answered 16/12, 2012 at 9:48 Comment(2)
Hi thanks for your reply. If I execute them synchronously the other operations will respond to cancel events? Lets suppose op A depends on op B and op B depends on op C, if I call cancel all operations op B and op C will stop at first isCancelled? Or only op A will respond correctly to it?Allyn
You'd need to have references to the operations that could be cancelled from some external source, or each one could detect a cancellation condition and stop executing. It's beginning to look like you may need to rethink how all this is structured.Starlastarlene

© 2022 - 2024 — McMap. All rights reserved.