views:

728

answers:

3

I have an NSOperationQueue on my main thread running a set of NSOperations (max concurrent set to 1) that I want to be able to cancel at any time. When I press a button I tell the queue to cancel all operations and wait until finished. This should hang the main thread until the operation queue is empty, however it is hanging my main thread indefinitely.

Here's the code I use to stop it:

...
[myQueue cancelAllOperations];
[myQueue waitUntilAllOperationsAreFinished];
return YES; // This line never gets called

Note: I need to use waitUntilAllOperationsAreFinished as further processes require that the queue be empty.

The strange thing is this is only occurring on the device. When running in the simulator it works as expected.

I have watched breakpoints and I can follow the currently running operation until it finishes. It detects [self isCancelled], stops what it's doing and zips through to the end of the main method. I can see that nothing in the operation is causing it to hang, and by cancelling all operations, none of the other operations should start, and the queue should finish. I have checked by adding breakpoints and none of the other operations start.

Why is this happening?

A: 

Perhaps the waitUntilAllOperationsArefFinished is causing the block... Maybe the operations all cancel and finish before the call to waitUntilAllOperationsArefFinished and then the queue is sat hanging, waiting for operations that are already finished to finish...?

I don't know this for a fact, but maybe try not calling waitUntilAllOperationsArefFinished.

Jasarien
+1  A: 

In any of your operations (or in any other thread), are you using -performSelectorOnMainThread:withObject:waitUntilDone:? -waitUntilAllOperationsAreFinished will block whatever thread it's called on until all operations are complete. Odds are, if you're calling this as the application is terminating, that thread would be the main thread. If the main thread is blocked, and one of the operations uses -performSelectorOnMainThread:withObject:waitUntilDone:, your application will freeze because the operation will never complete.

I've had this exact thing happen to me before. Unfortunately, it's pretty difficult to work around.

Brad Larson
Strangely this is occurring on a non-concurrent operation that doesn't make any calls on the main thread. However I've got a concurrent operation that does things on the main thread in between async calls and I think that's getting blocked! I don't really understand why this method is available if it could cause these issues. Unless it's meant to be called on another thread that isn't the main thread!? I'm trying to cancel my operations on app terminate and when it hangs it's causing a crash report due to termination times. Quite a tricky one! Any suggestions? Thanks for your answer!
Michael Waterfall
One way I've handled this is to cancel all background processes that would hit the main thread, then call -cancelAllOperations. Before any code that would perform a selector on the main thread in my operations, I add a check for isCancelled, which bails out of the operation at that point if true. I then sleep() for a short bit after the -cancelAllOperations message, and finally call -waitUntilAllOperationsAreFinished. If you're careful about shutting down everything that hits the main thread but this, you can make this work.
Brad Larson
Thanks Brad, that's pretty much how I've overcome it. `cancelAllOperations` followed by a sleep ~1s (to be sure!). My `NSOperations` check for `isCancelled` ALOT, and before anything that may take a while, so they should exit very quickly. Another little trick I've found incase my core data saves takes a while, I have used a global NSCondition, so I basically cancel, sleep, wait for signal (if needed). This holds the main thread perfectly, working like a dream! Thanks for your help!
Michael Waterfall
+2  A: 

You should never, ever block the main thread. It handles all your UI updates for one thing, for another as you have noted you managed to create a deadlock.

Instead, try creating a method like:

- (void) notifyOnFinish
{
   [myQueue waitUntilAllOperationsAreFinished];
   [self performSelectorOnMainThread:(queueEmpty) withObject:nil waitUntilDone:NO];
}

Then where you have your code now, call:

[myQueue cancelAllOperations];
[self performSelectorInBackground:@selector(notifyOnFinish) withObject:nil];

And in a queueEmpty method you just do whatever you want to do when the queue is emptied.

Basically, just create a background thread to block instead of the main thread.

Kendall Helmstetter Gelner
I need to block the main thread as the operations are cancelled on app terminate and I need to ensure they close correctly. They will cancel very fast so there's no issue of taking too long. So I need to block the main thread to ensure the background threads aren't just randomly stopped on app terminate.
Michael Waterfall
Blocking the main thread on exit is the one case that I can think of where this might be required, in order to guarantee that all of your operations will complete before the application is closed. A callback on a separate thread won't cut it, because that won't prevent the application from being stopped mid-operation.
Brad Larson
Then you need to eliminate any possibility of deadlock, my thinking would be a global synchronized flag that the main thread would set before blocking, and that all operations would check before attempting to perform a selector on the main thread.
Kendall Helmstetter Gelner