I've got a bunch of NSOperations added to a NSOperationQueue. The operation queue has the maxConcurrentOperationCount set to 1, so that the NSOperations run one after the other.
Now, in the completionBlock of a NSOperation I want to cancel all pending NSOperations by calling cancelAllOperations on the NSOperationQueue.
Is it safe to do this? Can I be sure that the start-method of the next operation is called only after the completionBlock of the previous operation has been fully executed? Or do the completionBlock of the previous operation and the task of the current operation run concurrently?
The reason why I'm asking: I use AFNetworking to execute a batch of AFHTTPRequestOperations and want to perform one request only if all previous requests of the batch were successful.
My findings below no longer seem to be true. I've re-run the tests on iOS 8 and iOS 9 and the completion block of an operation always runs concurrently with the next operation. Currently, I don't see a way to make an operation wait for the previous completion block to finish.
I just tried this scenario in a sample project. Here is the result:
If the NSOperationQueue's maxConcurrentOperationCount is set to 1, an NSOperation's completionBlock and the next NSOperation in the queue run simultaneously.
But, if every NSOperation is linked to its previous operation by calling addDependency:, the execution of an operation waits until the previous operation's completionBlock has finished.
So, if you want to cancel the next operation in the completionBlock of the current operation and be sure that it is cancelled before it is started, you have to set dependencies between the NSOperations by calling addDependency:
NSOperation establishes dependency only based on the completion states of operations, and not on the results of completed operations.
However, most of the scenarios that I encounter are such that, the execution of operations depend not only on the completion of some other operations, but also based on the results obtained from the completed operations.
I ended up doing like the below method, but still exploring if there is a better way:
1) Operation-A runs
2) Operation-A compeletes and its completionBlock runs
3) In the OperationA's completion block, check for the result obtained from Operation-A.
If result is X, create Operation-B and add to the queue.
If result is Y, create Operation-C and add to the queue.
If result is error, create Operation-D (usually an alert operation) and add to the queue
So, this ends up as a sequence of operations, that are dynamically added to the queue, depending on the result of completed operations.
I came up with another seemingly better way to ensure that an operaion is executed only if certain conditions (based on the results of previously finished operations) are met, else, the operation is cancelled.
One important consideration here is that the condition check for running an operation should not be coded inside the operation subclass, thus allowing the operation subclass to be poratble across different scenarios and apps.
Solution:
- Have a condition block property inside the subclass, and set whatever condition form where the operation is instantiated.
- Override "isReady" getter of the NSOperation subclass, check the condition there, and thus determine if its ready for execution.
- If [super isReady] is YES, which means the dependent operations are all finished, then evaluate the necessary condition.
- If the condition check is passed, return YES. Else, set isCancelled to YES and return YES for isReady
Code:
In the interface file have the block property:
typedef BOOL(^ConditionBlock)(void);
#property (copy) ConditionBlock conditionBlock;
In the implementation, override isReady, and cancelled:
#implementation ConditionalOperation
- (BOOL)isReady {
if([super isReady]) {
if(self.conditionBlock) {
if(!self.conditionBlock()) {
[self setCancelled:YES];
}
return YES;
} else {
return YES;
}
} else {
return NO;
}
}
Related
I need to make some API calls and I want to ensure that they come back in the order that they went out. Is this the proper flow to have that happen?
Create NSOperationQueue, set max concurrent operations to 1
Create URL String to API
Create NSOperation block, call method to call API, pass URL string
Add NSOperation to NSOperationQueue
This is where I get confused. Setting the max concurrent operations to 1 essentially makes NSOperationQueue into a synchronous queue, only 1 operation gets called at a time. However, each operation is going to make a NSURLSession call, which is async. How can I ensure that the next operation doesn't run until I have finished with the first? (By finish I want to store the returned JSON in a NSArray, adding each additional returned JSON to that array).
The proper way to ensure that NSOperations run in order is to add dependencies. Dependencies are powerful as they allow ordering of different operations on different queues. You can make an API call or data processing on a background queue; when complete, a dependent operation can update the UI on the main thread.
let operation1 = NSBlockOperation {
print("Run First - API Call")
}
let operation2 = NSBlockOperation {
print("Run Second - Update UI")
}
operation2.addDependency(operation1)
let backgroundQueue = NSOperationQueue()
backgroundQueue.addOperation(operation1)
NSOperationQueue.mainQueue().addOperation(operation2)
// operation1 will finish before operation2 is called, regardless of what queue they're in
See Apple Docs on addDependency in NSOperation here: https://developer.apple.com/library/mac/documentation/Cocoa/Reference/NSOperation_class/index.html#//apple_ref/occ/cl/NSOperation
Also, be careful with assuming that maxConcurrentOperationCount = 1 as all that it does is ensure that only 1 operation will run at a time. This does NOT ensure the order of the queue. An operation with a higher priority will likely run first.
I've started working with CloudKit and finally started using subclassed NSOperation for most of my async stuff.
How ever, I have two questions.
How can I mark an operation as failed? That is, if operation A fails, I don't wan't its dependent operations to run. Can I just not mark it as isFinished? What happens to the unexecuted items already in the queue?
What would be the recommended route to take if I would like something like a try, catch, finally. The end goal is to have one last operation that can display some UI with information of success or report errors back to the user?
isFinished means your operations complete execution, you can cancel an operation but that means your operation is canceled and that could be done without even executing the operation and you can check that by calling isCanceled, if you want spefically failure and success flags after executing an NSOperation then in subclass add isFailure property and check in dependent operation before executing, and cancel that if isFailure is set to true.
You can add dependency on operation and check there status, and if all are successful just update UI on main thread or report and error.
Update
You can keep and array of dependent operations and when on failure you can cancel those operations.
Add the operations to a queue. When one of them fails call cancel on the queue. Future CKOperations will error on start with "operation was cancelled before it started" and any block operations won't even run.
One way is to use KVO on the queue's operationCount (Example on Github) and wait until it is zero. Then if you got an error at any stage (and captured it) you can make a final callback operation ended with the error. However, usually you will end up wanting to display a different message depending on the operation that caused the error, in which case it is best to handle them when they happen, rather than wait until the end when you then need to figure out which error came from which operation.
Overview:
When an operation fails (based on your business logic) and you want to abort all dependant operations then you can cancel dependant operations manually
Order in which you cancel operation is important as cancelling an operation would allow for the dependant operations to start (as the dependancy condition has been broken).
In order to all this you need to have variables holding each of those dependant operations, so that you cancel them in the order you intend.
Code:
var number = 0
let queue = OperationQueue()
let b = BlockOperation()
let c = BlockOperation()
let d = BlockOperation()
let a = BlockOperation {
if number == 1 {
//Assume number should not be 1
//If number is 1, then it is considered as a failure
//Cancel the remaining operations manually in the reverse order
//Reverse order is important because if you cancelled b first, it would start c
d.cancel()
c.cancel()
b.cancel()
}
}
b.addDependency(a)
c.addDependency(b)
d.addDependency(c)
queue.addOperation(a)
queue.addOperation(b)
queue.addOperation(c)
queue.addOperation(d)
I'm using NSOperation to perform two operations. The first operation is loading the data from Internet, while the second operation is updating the UI.
However, if the viewDidDisappear function is triggered by user, how can I stop the data loading process?
I tried
[taskQueue cancellAllOperations],
but this function only marks every operation inside as cancelled while not literally cancel the executing process.
Could anyone please give some suggestions? Thanks in Advance.
AFAIK, there's no direct way to cancel an already executing NSOperation. But you can cancel the taskQueue like you're doing.
[taskQueue cancellAllOperations];
And inside the operation block, periodically (in between logically atomic block of code) check for isCancelled to decide whether to proceed further.
NSBlockOperation *loadOp = [[NSBlockOperation alloc]init];
__weak NSBlockOperation *weakRefToLoadOp = loadOp;
[loadOp addExecutionBlock:^{
if (!weakRefToLoadOp.cancelled) {
// some atomic block of code 1
}
if (!weakRefToLoadOp.cancelled) {
// some atomic block of code 2
}
if (!weakRefToLoadOp.cancelled) {
// some atomic block of code 3
}
}];
The NSOperation's block should be carefully divided into sub-block, such that it is safe to discontinue the execution of rest of the block. If required, you should also rollback the effects of sub-blocks executed so far.
if (!weakRefToLoadOp.cancelled) {
// nth sub-block
}
else {
//handle the effects of so-far-executed (n-1) sub-blocks
}
Thanks sincerely for your answer. But I find out that actually
[self performSelectorInBackground:#selector(httpRetrieve) withObject:nil];
solve my problem. The process don't have to be cancelled. And feels like NSOpertaions is not running in the background. Thus, back to super navigation view while the nsoperation is still running, the UI will become stuck!
I'm writing an app where I've got a long running server-syncronization task running in the background, and I'd like to use NSOperation and NSOperationQueue for this. I'm leaning this way, since I need to ensure only one synchronisation operation is running at once.
My question arises since my architecture is built around NSNotifications; my synchronisation logic proceeds based on these notifications. From what I can see, NSOperation logic needs to be packed into the main method. So what I'm wondering is if there is any way to have an NSOperation finish when a certain notification is received. I suspect this is not the case, since I haven't stumbled upon any examples of this usage, but I figured I'd ask the gurus in here. Does an NSOperation just finish when the end of the main method is reached?
There is no reason a NSOperation cannot listen for a notification on the main thread, but either the finish logic must be thread safe, or the operation must keep track of its current thread.
I would recommend a different approach. Subclass NSOperation to support a method like -finishWithNotification: Have a queue manager that listens for the notification. It can iterate through its operations finishing any operations which respond to -finishWithNotification:.
- (void)handleFinishNotification:(NSNotification *)notification
{
for (NSOperation *operation in self.notificationQueue) {
if ([operation isKindOfClass:[MYOperation class]]) {
dispatch_async(self.notificationQueue.underlyingQueue), ^{
MYOperation *myOperation = (MYOperation *)operation;
[myOperation finishWithNotification:notification];
});
}
}
}
If I understood you correctly concurrent NSOperation is what you need.
Concurrent NSOperation are suitable for long running background/async tasks.
NSOperation Documentation
See: Subclassing Notes & Operation Dependencies Section
EDIT:(Adding more explanation)
Basically concurrent operations do not finish when main method finishes. Actually what concurrent operation mean is that the control will return to calling code before the actual operation finishes. The typical tasks that are done in start method of concurrent operation are: Mark operation as executing, start the async work(e.g. NSURLConnection async call) or spawn a new thread which will perform bulk of the task. And RETURN.
When the async task finishes mark the operation as finished.
I'm googling like crazy and still confused about this.
I want to download an array of file urls to disk, and I want to update my view based on the bytes loaded of each file as they download. I already have something that will download a file, and report progress and completion via blocks.
How can I do this for each file in the array?
I'm ok doing them one at a time. I can calculate the total progress easily that way:
float progress = (numCompletedFiles + (currentDownloadedBytes / currentTotalBytes)) / totalFiles)
I mostly understand GCD and NSOperations, but how can you tell an operation or dispatch_async block to wait until a callback is called before being done? It seems possible by overriding NSOperation, but that seems like overkill. Is there another way? Is it possible with just GCD?
I'm not sure if I understand you correctly, but perhaps you need dispatch semaphores to achieve your goal. In one of my projects I use a dispatch semaphore to wait until another turn by another player is completed. This is partially the code I used.
for (int i = 0; i < _players.count; i++)
{
// a semaphore is used to prevent execution until the asynchronous task is completed ...
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
// player chooses a card - once card is chosen, animate choice by moving card to center of board ...
[self.currentPlayer playCardWithPlayedCards:_currentTrick.cards trumpSuit:_trumpSuit completionHandler:^ (WSCard *card) {
BOOL success = [self.currentTrick addCard:card];
DLog(#"did add card to trick? %#", success ? #"YES" : #"NO");
NSString *message = [NSString stringWithFormat:#"Card played by %#", _currentPlayer.name];
[_messageView setMessage:message];
[self turnCard:card];
[self moveCardToCenter:card];
// send a signal that indicates that this asynchronous task is completed ...
dispatch_semaphore_signal(sema);
DLog(#"<<< signal dispatched >>>");
}];
// execution is halted, until a signal is received from another thread ...
DLog(#"<<< wait for signal >>>");
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
dispatch_release(sema);
DLog(#"<<< signal received >>>");
dispatch groups are the GCD facility designed to track completion of a set of independent or separately async'd blocks/tasks.
Either use dispatch_group_async() to submit the blocks in question, or dispatch_group_enter() the group before triggering the asynchronous task and dispatch_group_leave() the group when the task has completed.
You can then either get notified asynchronously via dispatch_group_notify() when all blocks/tasks in the group have completed, or if you must, you can synchronously wait for completion with dispatch_group_wait().
I just wanted to note that I did get it working by subclassing NSOperation and making it a "concurrent" operation. (Concurrent in this context means an async operation that it should wait for before marking it as complete).
http://www.dribin.org/dave/blog/archives/2009/05/05/concurrent_operations/
Basically, you do the following in your subclass
override start to begin your operation
override isConcurrent to return YES
when you finish, make sure isExecuting and isFinished change to be correct, in a key-value compliant manner (basically, call willChangeValueForKey: and didChangeValueForKey: for isFinished and isExecuting
And in the class containing the queue
set the maxConcurrentOperationCount on the NSOperationQueue to 1
add an operation after all your concurrent ones which will be triggered once they are all done