I have a chain of many NSBlockOperations with dependencies. If one operation early in the chain fails - I want the other operations to not run. According to docs, this should be easy to do from the outside - if I cancel an operation, all dependent operations should automatically be cancelled.
However - if only the execution-block of my operation "knows" that it failed, while executing - can it cancel its own work?
I tried the following:
NSBlockOperation *op = [[NSBlockOperation alloc] init];
__weak NSBlockOperation *weakOpRef = op;
[takeScreenShot addExecutionBlock:^{
LOGInfo(#"Say Cheese...");
if (some_condition == NO) { // for some reason we can't take a photo
[weakOpRef cancel];
LOGError(#"Photo failed");
}
else {
// take photo, process it, etc.
LOGInfo(#"Photo taken");
}
}];
However, when I run this, other operations dependent on op are executed even though op was cancelled. Since they are dependent - surely they're not starting before op finished, and I verified (in debugger and using logs) that isCancelled state of op is YES before the block returns. Still the queue executes them as if op finished successfully.
I then further challenged the docs, like thus:
NSOperationQueue *myQueue = [[NSOperationQueue alloc] init];
NSBlockOperation *op = [[NSBlockOperation alloc] init];
__weak NSBlockOperation *weakOpRef = takeScreenShot;
[takeScreenShot addExecutionBlock:^{
NSLog(#"Say Cheese...");
if (weakOpRef.isCancelled) { // Fail every once in a while...
NSLog(#"Photo failed");
}
else {
[NSThread sleepForTimeInterval:0.3f];
NSLog(#"Photo taken");
}
}];
NSOperation *processPhoto = [NSBlockOperation blockOperationWithBlock:^{
NSLog(#"Processing Photo...");
[NSThread sleepForTimeInterval:0.1f]; // Process
NSLog(#"Processing Finished.");
}];
// setup dependencies for the operations.
[processPhoto addDependency: op];
[op cancel]; // cancelled even before dispatching!!!
[myQueue addOperation: op];
[myQueue addOperation: processPhoto];
NSLog(#">>> Operations Dispatched, Wait for processing");
[eventQueue waitUntilAllOperationsAreFinished];
NSLog(#">>> Work Finished");
But was horrified to see the following output in the log:
2020-11-05 16:18:03.803341 >>> Operations Dispatched, Wait for processing
2020-11-05 16:18:03.803427 Processing Photo...
2020-11-05 16:18:03.813557 Processing Finished.
2020-11-05 16:18:03.813638+0200 TesterApp[6887:111445] >>> Work Finished
Pay attention: the cancelled op was never run - but the dependent processPhoto was executed, despite its dependency on op.
Ideas anyone?
OK. I think I solved the mystery. I just misunderstood the [NSOperation cancel] documentation.
it says:
In macOS 10.6 and later, if an operation is in a queue but waiting on
unfinished dependent operations, those operations are subsequently
ignored. Because it is already cancelled, this behavior allows the
operation queue to call the operation’s start method sooner and clear
the object out of the queue. If you cancel an operation that is not in
a queue, this method immediately marks the object as finished. In each
case, marking the object as ready or finished results in the
generation of the appropriate KVO notifications.
I thought if operation B depends on operation A - it implies that if A is canceled (hence - A didn't finish its work) then B should be cancelled as well, because semantically it can't start until A completes its work.
Apparently, that was just wishful thinking...
What documentation says is different. When you cancel operation B (which depends on operation A), then despite being dependent on A - it won't wait for A to finish before it's removed from the queue. If operation A started, but hasn't finished yet - canceling B will remove it (B) immediately from the queue - because it will now ignore dependencies (the completion of A).
Soooo... to accomplish my scheme, I will need to introduce my own "dependencies" mechanism. The straightforward way is by introducing a set of boolean properties like isPhotoTaken, isPhotoProcessed, isPhotoColorAnalyzed etc. Then, an operation dependent on these pre-processing actions, will need to check in its preamble (of execution block) whether all required previous operations actually finished successfully, else cancel itself.
However, it may be worth subclassing NSBlockOperation, overriding the logic that calls 'start' to skip to finished if any of the 'dependencies' has been cancelled!
Initially I thought this is a long shot and may be hard to implement, but fortunately, I wrote this quick subclass, and it seems to work fine. Of course deeper inspection and stress tests are due:
#interface MYBlockOperation : NSBlockOperation {
}
#end
#implementation MYBlockOperation
- (void)start {
if ([[self valueForKeyPath:#"dependencies.#sum.cancelled"] intValue] > 0)
[self cancel];
[super start];
}
#end
When I substitute NSBlockOperation with MYBlockOperation in the original question (and my other tests, the behaviour is the one I described and expected.
If you cancel an operation you just hint that it is done, especially in long running tasks you have to implement the logic yourself. If you cancel something the dependencies will consider the task finished and run no problem.
So what you need to do is have some kind of a global synced variable that you set and get in a synced fashion and that should capture your logic. Your running operations should check that variable periodically and at critical points and exit themselves. Please don't use actual global but use some common variable that all processes can access - I presume you will be comfortable in implementing this?
Cancel is not a magic bullet that stop the operation from running, it is merely a hint to the scheduler that allows it to optimise stuff. Cancel you must do yourself.
This is explanation, I can give sample implementation of it but I think you are able to do that on your own looking at the code?
EDIT
If you have a lot of blocks that are dependent and execute sequentially you do not even need an operation queue or you only need a serial (1 operation at a time) queue. If the blocks execute sequentially but are very different then you need to rather work on the logic of NOT adding new blocks once the condition fails.
EDIT 2
Just some idea on how I suggest you tackle this. Of course detail matters but this is also a nice and direct way of doing it. This is sort of pseudo code so don't get lost in the syntax.
// Do it all in a class if possible, not subclass of NSOpQueue
class A
// Members
queue
// job1
synced state cancel1 // eg triggered by UI
synced state counter1
state calc1 that job 1 calculates (and job 2 needs)
synced state cancel2
synced state counter2
state calc2 that job 2 calculated (and job 3 needs)
...
start
start on queue
schedule job1.1 on (any) queue
periodically check cancel1 and exit
update calc1
when done or exit increase counter1
schedule job1.2 on (any) queue
same
schedule job1.3
same
wait on counter1 to reach 0
check cancel1 and exit early
// When you get here nothing has been cancelled and
// all you need for job2 is calculated and ready as
// state1 in the class.
// This is why state1 need not be synced as it is
// (potentially) written by job1 and read by job2
// so no concurrent access.
schedule job2.1 on (any) queue
and so on
This is to me most direct and ready for future development way of doing it. Easy to maintain and understand and so on.
EDIT 3
Reason I like and prefer this is because it keeps all your interdependent logic in one place and it is easy to later add to it or calibrate it if you need finer control.
Reason I prefer this to e.g. subclassing NSOp is that then you spread out this logic into a number of already complex subclasses and also you loose some control. Here you only schedule stuff after you've tested some condition and know that the next batch needs to run. In the alternative you schedule all at once and need additional logic in all subclasses to monitor progress of the task or state of the cancel so it mushrooms quickly.
Subclassing NSOp I'd do if the specific op that run in that subclass needs calibration, but to subclass it to manage the interdependencies adds complexity I recon.
(Probably final) EDIT 4
If you made it this far I am impressed. Now, looking at my proposed piece of (pseudo) code you might see that it is overkill and that you can simplify it considerably. This is because the way it is presented, the different components of the whole task, being task 1, task 2 and so on, appear to be disconnected. If that is the case there are indeed a number of different and simpler ways in which you can do this. In the reference I give a nice way of doing this if all the tasks are the same or very similar or if you have only a single subsubtask (e.g. 1.1) per subtask (e.g. 1) or only a single (sub or subsub) task running at any point in time.
However, for real problems, you will probably end up with much less of a clean and linear flow between these. In other words, after task 2 say you may kick of task 3.1 which is not required by task 4 or 5 but only needed by task 6. Then the cancel and exit early logic already becomes tricky and the reason I do not break this one up into smaller and simpler bits is really because like here the logic can (easily) also span those subtasks and because this class A represents a bigger whole e.g. clean data or take pictures or whatever your big problem is that you try to solve.
Also, if you work on something that is really slow and you need to squeeze out performance, you can do that by figuring out the dependencies between the (sub and subsub) tasks and kick them off asap. This type of calibration is where (real life) problems that took way too long for the UI becomes doable as you can break them up and (non-linearly) piece them together in such a way that you can traverse them in a most efficient way.
I've had a few such a problems and, one in particular I am thinking know became extremely fragile and the logic difficult to follow, but this way I was able to bring the solution time down from an unacceptable more than a minute to just a few seconds and agreeable to the users.
(This time really almost the final) EDIT 5
Also, the way it is presented here, as you make progress in solving the problem, at those junctures between say task 1 and 2 or between 2 and 3, those are the places where you can update your UI with progress and parts of the full solution as it trickles in from all the various (sub and subsub) tasks.
(The end is coming) EDIT 6
If you work on a single core then, except for the interdependencies between tasks, the order in which you schedule all those sub and subsub tasks do not matter since execution is linear. The moment you have multiple cores you need to break the solution up into as small as possible subtasks and schedule the longer running ones asap for performance. The performance squeeze you get can be significant but comes at the cost of increasingly complex flow between all the small little subtasks and in the way in which you handle the cancel logic.
Related
Lets say, i have a complex calculation running in NSOperation block. I have paused it. Closed the app. Then restarted the app. Can i recover the last state and continue from there?
Is there existing solution for such a problem or it can be only custom built for certain purposes?
The question is a bit vague, so it's hard to say without knowing all of the code in play. With that said, I may approach the problem by:
Option 1. In your subclass of NSOperation, add your own atomic KVO property "isPaused". Within the operation itself, observe that property and handle accordingly if it ever changes.
Option 2. Are you ever suspending the Operation Queue itself? If so, consider observing that property from within your operations, and each one independently can take action if that value changes.
Option 3. Cancel all operations in the queue, and if the view appears again, just restart with new operations.
Overall, though, there is no magic bullet for pausing operations already in progress. You'll have to bake your own solution. The damage shouldn't be too bad though.
Suspending and Resuming Queues If you want to issue a temporary halt to the execution of operations, you can suspend the corresponding operation queue using the setSuspended: method.
Suspending a queue does not cause already executing operations to pause in the middle of their tasks. It simply prevents new operations from being scheduled for execution. You might suspend a queue in response to a user request to pause any ongoing work, because the expectation is that the user might eventually want to resume that work.
For more detail Refer this link apple docs: http://developer.apple.com/library/mac/#documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationObjects/OperationObjects.html
When subclassing NSOperation to get a little chunk of work done, I've found out it's pretty easy to deadlock. Below I have a toy example that's pretty easy to understand why it never completes.
I can only seem to think through solutions that prevent the deadlock from the caller perspective, never the callee. For example, the caller could continue to run the run loop, not wait for finish, etc. If the main thread needs to be message synchronously during the operation, I'm wondering if there is a canonical solution that an operation subclasser can implement to prevent this type of deadlocking. I'm only just starting to dip my toe in async programming...
#interface ToyOperation : NSOperation
#end
#implementation ToyOperation
- (void)main
{
// Lots of work
NSString *string = #"Important Message";
[self performSelector:#selector(sendMainThreadSensitiveMessage:) onThread:[NSThread mainThread] withObject:string waitUntilDone:YES];
// Lots more work
}
- (void)sendMainThreadSensitiveMessage:(NSString *)string
{
// Update the UI or something that requires the main thread...
}
#end
- (int)main
{
ToyOperation *op = [[ToyOperation alloc] init];
NSOperationQueue *opQ = [[NSOperationQueue alloc] init];
[opQ addOperations: #[ op ] waitUntilFinished:YES]; // Deadlock
return;
}
If the main thread needs to be message synchronously during the
operation, I'm wondering if there is a canonical solution that an
operation subclasser can implement to prevent this type of
deadlocking.
There is. Never make a synchronous call to the main queue. And a follow-on: Never make a synchronous call from the main queue. And, really, it can be summed up as Never make a synchronous call from any queue to any other queue.
By doing that, you guarantee that the main queue is not blocked. Sure, there may be an exceptional case that tempts you to violate this rule and, even, cases where it really, truly, is unavoidable. But that very much should be the exception because even a single dispatch_sync() (or NSOpQueue waitUntilDone) has the potential to deadlock.
Of course, data updates from queue to queue can be tricky. There are several options; a concurrency safe data layer (very hard), only passing immutable objects or copies of the data (typically to the main queue for display purposes -- fairly easy, but potentially expensive), or you can go down the UUID based faulting like model that Core Data uses. Regardless of how you solve this, the problem isn't anything new compared to any other concurrency model.
The one exception is when replacing locks with queues (For example, instead of using #synchronized() internally to a class, use a serial GCD queue and use dispatch_sync() to that queue anywhere that a synchronized operation must take place. Faster and straightforward.). But this isn't so much an exception as solving a completely different problem.
Is it safe to add an NSOperationQueue to an NSOperation, and then add this operation to another NSOperationQueue?
Here is some code to visualize what I am trying to do.
NSOperationQueue *mainQueue = [NSOperationQueue alloc] init];
// Here I declare some NSBlockOperation's, i.e. parseOperation1-2-3
// and also another operation called zipOperation, which includes
// an NSOperationQueue itself. This queue takes the processed (parsed) files
// and write them to a single zip file. Each operation's job is to write the data
// stream and add it to the zip file. After all operations are done,
// it closes the zip.
[zipOperation addDependency:parseOperation1];
[zipOperation addDependency:parseOperation2];
[zipOperation addDependency:parseOperation3];
[mainQueue addOperation:parseOperation1];
[mainQueue addOperation:parseOperation2];
[mainQueue addOperation:parseOperation3];
[mainQueue addOperation:zipOperation];
I have used this approach and have it running in live code deployed on the App Store. I haven't experienced any issues during development or in the last 2 months since the code has been live.
In my case I had a high level series of operations, some of which contained a set of sub operations. Rather than expose the detail of each sub operation into the high level code, I created NSOperations which themselves contained NSOperationQueues and enqueued their own sub operations. The code I ended up with was much cleaner and easier to maintain.
I read extensively into NSOperation and have not seen any commentary that warns against this approach. I reviewed a lot of information online, the Apple documentation, and WWDC videos.
The only possible "drawback" might be the added complexity of understanding and implementing a Concurrent operation. Embedding an NSOperationQueue in an NSOperation means that operation becomes Concurrent.
So that's a 'YES' from me.
Additional details about concurrent operations:
An NSOperationQueue calls the start method on a normal (non-concurrent) NSOperation and expects the operation to be finished by the time the start call returns. For instance some piece of code you supplied to NSBlockOperation is complete at the end of the block.
If the work will not be finished by the time the start call returns then you configure the NSOperation as a Concurrent operation, so the NSOperationQueue knows that it has to wait until you tell it that the operation is finished at some later point in time.
For example, concurrent operations are often used to run asynchronous network calls; the start method only starts the network call, which then runs in the background, and calls back to the operation when its finished. You then change the isFinished property of the NSOperation to flag that the work is now complete.
So.... Normally when you add operations to an NSOperationQueue that queue runs those operations in the background. So if you put an NSOperationQueue inside an NSOperation then that operations work will be done in the background. Therefore the operation is concurrent and you need to flag when the internal NSOperationQueue has finished processing all it's operations.
Alternatively there are some methods on NSOperationQueue such as waitUntilAllOperationsAreFinished which could be used to ensure all the work was done before the start call returns, however these involve blocking threads and I avoided them, you may feel more comfortable with that approach, and making sure you don't have any side effects from blocking threads.
In my case I was already familiar with Concurrent operations so it was straightforward just to set it up as a Concurrent operation.
Some documentation about concurrent operations:
Concurrency Programming Guide: Configuring Operations for Concurrent Execution
In this example they are detaching a thread to perform work in the background, in our case we would be starting the NSOperationQueue here.
FACTS:
I have a method executing on a background thread :
[currentGame performSelectorInBackground:#selector(playOn:) withObject:self];
This method basically contains a while loop that keeps on executing until the user clicks on a Quit button:
-(void) playOn: (UIViewController*) thisViewController
{
while(!quitButtonPressed)
{
// this is my method's main loop
}
}
PROBLEM:
If the user clicks on Quit somewhere in the middle of the above loop
the rest of the loop would have to execute before it checks the BOOL once again and eventually stops. In order to prevent that from
happening and have the while-loop stop as soon as the user clicks on
Quit, I guess I could also add many if(quitButtonPressed) break;
here and there in my while loop in order to semi-constantly check and "immediately" break away if needed. However, this doesn't seem
very clever or practical from a design perspective given the size of
the above main while-loop and the fact that it contains many smaller
while-loops inside of it (the number of if.. break; I would have to
add would be quite big and could make things quite complicated to
figure out..)
POSSIBLE SOLUTION (but is it the right one?) :
So I was thinking that the best way would be to stop/cancel the
background thread on which the above method's while loop is executing,
instead of the while-loop itself, inside the method, the moment the
user clicks on Quit
Is this, or something similar (i.e. a better suggestion), possible and
how exactly could I do this?
POSSIBLE IMPLEMENTATION OF ABOVE SOLUTION:
I could create this new method:
-(void)checkQuitButton
{
while(!quitButtonPressed)
{
//wait
}
if(quitButtonPressed)
{
// stop this thread-->[currentGame performSelectorInBackground:#selector(playOn:) withObject:self];
// this is the method I'm looking for
}
}
And then I could start executing the above and my previous main method concurrently on two separate background threads as follows:
[currentGame performSelectorInBackground:#selector(playOn:) withObject:self];
[currentGame performSelectorInBackground:#selector(checkQuitButton) withObject:nil];
While the game while-loop is being executed another while-loop is checking the QuitButton at the same time. But is there a method that I can actually call in order to cancel what was started here:
[currentGame performSelectorInBackground:#selector(playOn:) withObject:self];
?
The correct solution is to periodically check for a "stop" flag. Abruptly terminating a thread provides no opportunity to clean up resources. In short, you would leak memory terribly.
But the deeper issue is that you almost certainly should not have this kind of thread. It strongly suggests an incorrect design. In iOS, most background operations should take the form of focused operations, implemented either with NSOperation or blocks with Grand Central Dispatch. You should very, very seldom need a long lived thread that is performing many different kinds of functions. Within your operation, it should be fairly straightforward where to put the "check for cancel" statements.
There is also almost no case where you should use performSelectorInBackground:. It is an incredibly dangerous method that gives you very little control. Instead, read the Concurrency Programming Guide for guidance on how to properly implement background operations. Pay special attention to the section "Migrating Away From Threads."
i need to do a series of url calls (fetching WMS tiles). i want to use a LIFO stack so the newest url call is the most important. i want to display the tile on the screen now, not a tile that was on the screen 5 seconds ago after a pan.
i can create my own stack from a NSMutableArray, but i'm wondering if a NSOperationQueue can be used as a LIFO stack?
You can set the priority of operations in an operation queue using -[NSOperation setQueuePriority:]. You'll have to rejigger the priorities of existing operations each time you add an operation, but you can achieve something like what you're looking for. You'd essentially demote all of the old ones and give the newest one highest priority.
Sadly I think NSOperationQueues are, as the name suggests, only usable as queues — not as stacks. To avoid having to do a whole bunch of manual marshalling of tasks, probably the easiest thing is to treat your queues as though they were immutable and mutate by copying. E.g.
- (NSOperationQueue *)addOperation:(NSOperation *)operation toHeadOfQueue:(NSOperationQueue *)queue
{
// suspending a queue prevents it from issuing new operations; it doesn't
// pause any already ongoing operations. So we do this to prevent a race
// condition as we copy operations from the queue
queue.suspended = YES;
// create a new queue
NSOperationQueue *mutatedQueue = [[NSOperationQueue alloc] init];
// add the new operation at the head
[mutatedQueue addOperation:operation];
// copy in all the preexisting operations that haven't yet started
for(NSOperation *operation in [queue operations])
{
if(!operation.isExecuting)
[mutatedQueue addOperation:operation];
}
// the caller should now ensure the original queue is disposed of...
}
/* ... elsewhere ... */
NSOperationQueue *newQueue = [self addOperation:newOperation toHeadOfQueue:operationQueue];
[operationQueue release];
operationQueue = newQueue;
It seems at present that releasing a queue that is still working (as will happen to the old operation queue) doesn't cause it to cancel all operations, but that's not documented behaviour so probably not trustworthy. If you want to be really safe, key-value observe the operationCount property on the old queue and release it when it goes to zero.
I'm not sure if you're still looking a solution, but I've the same problem has been bugging me for a while, so I went ahead and implemented an operation stack here: https://github.com/cbrauchli/CBOperationStack. I've used it with a few hundred download operations and it has held up well.
Sadly, you cannot do that without running into some tricky issues, because:
Important You should always configure dependencies before running your operations or adding them to an operation queue. Dependencies added afterward may not prevent a given operation object from running. (From: Concurrency Programming Guide: Configuring Interoperation Dependencies)
Take a look at this related question: AFURLConnectionOperation 'start' method gets called before it is ready and never gets called again afterwards
Found a neat implementation of stack/LIFO features on top of NSOperationQueue. It can be used as a category that extends NSOperationQueue or an NSOperationQueue LIFO subclass.
https://github.com/nicklockwood/NSOperationStack
The easiest way would be to separate your operations and data you will be processing, so you can add operations to NSOperationQueue as usual and then take data from a stack or any other data structure you need.
var tasks: [MyTask]
...
func startOperation() {
myQueue.addOperation {
guard let task = tasks.last else {
return
}
tasks.removeLast()
task.perform()
}
}
Now, obviously, you might need to ensure that tasks collection can be used concurrently, but it's a much more common problem with lots of pre-made solutions than hacking your way around NSOperationQueue execution order.