Can you use cancel/isCancelled with GCD/dispatch_async?
I've been wondering, can you use cancel/cancelAllOperations/.isCancelled with a thread you have launched with GCD?
Currently, I just use a boolean as a flag, to cancel the background process.
Let's say you want to do a lot of processing in the background, while keeping the UI responsive so that you can catch a cancel button (or animate something to show the processor is working). Here's how we do it...
@interface AstoundingView : UIView
{
BOOL pleaseAbandonYourEfforts;
blah
}
@implementation AstoundingView
//
// these are the foreground routines...
// begin, abandon and all-done
//
-(void)userHasClickedToBuildASpaceship
{
[YourUIStateMachine buildShip];
[self procedurallyBuildEnormousSpaceship];
}
-(void)userHasClickedToAbandonBuildingTheSpaceship
{
[YourUIStateMachine inbetween];
pleaseAbandonYourEfforts = false; // that's it!
}
-(void)attentionBGIsAllDone
{
// you get here when the process finishes, whether by completion
// or if we have asked it to cancel itself.
[self typically setNeedsDisplay, etc];
[YourUIStateMachine nothinghappening];
}
//
// these are the background routines...
// the kickoff, the wrapper, and the guts
//
// The wrapper MUST contain a "we've finished" message to home
// The guts can contain messages to home (eg, progress messages)
//
-(void)procedurallyBuildEnormousSpaceship
{
// user has clicked button to build new spaceship
pleaseAbandonYourEfforts = FALSE;
dispatch_async(
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0),
^{ [self actuallyProcedurallyBuildInBackground]; }
);
// as an aside, it's worth noting that this does not work if you
// use the main Q rather than a global Q as shown.
// Thus, this would not work:
// dispatch_async(dispatch_get_main_queue(), ^{ ...; });
}
-(void)actuallyProcedurallyBuildInBackground
{
// we are actually in the BG here...
[self setUpHere];
// set up any variables, contexts etc you need right here
// DO NOT open any variables, contexts etc in "buildGuts"
// when you return back here after buildGuts, CLEAN UP those
// variables, contexts etc at this level.
// (using this system, you can nest as deep as you want, and the
// one CHECKER pseudocall will always take you right out.
// You can insert CHECKERs anywhere you want.)
[self buildGuts];
// Note that any time 'CHECKER' "goes off', you must fall-
// through to exactly here. This is the common fall-through point.
// So we must now tidy-up, to match setUpHere.
[self wrapUpHere];
// when you get to here, we have finished (or, the user has cancelled
// the background operation)
// Whatever technique you use,
// MAKE SURE you clean up all your variables/contexts/etc before
// abandoning the BG process.
// and then you must do this......
// we have finished. it's critical to let the foreground know NOW,
// or else it will sit there for about 4 to 6 seconds (on 4.3/4.2)
// doing nothing until it realises you are done
dispatch_sync(
dispatch_get_main_queue(),
^{[self attentionBGIsAllDone];} // would setneedsdisplay, etc
);
return;
}
-(void)buildGuts
{
// we are actually in the BG here...
// Don't open any local variables in here.
CHECKER
[self blah blah];
CHECKER
[self blah blah];
CHECKER
[self blah blah];
// to get stuff done from time to time on the UI, something like...
CHECKER
dispatch_sync(
dispatch_get_main_queue(),
^{[supportStuff pleasePostMidwayImage:
[UIImage imageWithCGImage:halfOfShip] ];}
);
CHECKER
[self blah blah];
CHECKER
[self blah blah];
CHECKER
[self blah blah];
for ( i = 1 to 10^9 )
{
CHECKER
[self blah blah];
}
CHECKER
[self blah blah];
CHECKER
[self blah blah];
CHECKER
[self blah blah];
return;
}
and CHECKER does nothing more than check it the flag is true...
#define CHECKER if ( pleaseAbandonYourEfforts == YES ) \
{NSLog(@"Amazing Interruption System Working!");return;}
This all works perfectly.
But ........ is it possible to use cancel/cancelAllOperations/.isCancelled with this type of use of GCD?
What's the story here? Cheers.
PS - for any beginners using this "six part" background template.
Note that as BJ highlights below, whenever you break out of the bg process...
you must clean up any variables you have open!
In my idiom, you must allocate all variable, contexts, memory, etc, specifically in "setUpHere". And you must release them in "wrapUpHere". (This idiom continues to work if you go deeper and deeper, while in the BG.)
Alternately, do exactly what BJ shows in his example. (If you use BJ's method, be careful if you go deeper.)
Whatever method you use, you must clean up any variables/contexts/memory you have open, when you break out of the BG process. Hope it helps someone, sometime!
Solution 1:
GCD does not have built-in support for cancellation; if it's important to be able to cancel your background task, then checking a flag like you've demonstrated is an acceptable solution. However, you may want to evaluate how quickly the cancellation needs to respond; if some of those methods calls are fairly short, you may be able to get away with checking less frequently.
You asked whether you could use the NSOperation flags to support cancellation. The answer is no. GCD is not based on NSOperation. In fact, in Snow Leopard NSOperation and NSOperationQueue were re-implemented to use GCD internally. So the dependency is the other way around. NSOperation is a higher level construct than GCD. Even if you were to use NSOperation, though, your implementation of cancellation would be largely the same; you'd still have to check self.isCancelled
periodically to see whether you should abandon the space ship construction.
The only concern I have with your implementation of the CHECKER
macro is that it implements an unexpected return
. As such, you have to be careful about memory leaks. If you've set up your own NSAutoreleasePool on the background thread, you need to drain
it before returning. If you've alloc
ed or retain
ed any objects, you may need to release
them before returning.
Since all that cleanup needs to happen at every check, you may want to consider moving toward a single return point. One way to do this would be to wrap each of your method calls in a if (pleaseAbandonYourEfforts == NO) { }
block. This would let you quickly fall through to the end of the method once cancellation was requested, and keep your cleanup in a single location. Another option, though some may dislike it, would be to make the macro use call goto cleanup;
and define a cleanup:
label near the end of the method where you release anything that needs to be released. Some people dislike using goto
in an almost religious way, but I've found that a forward jump to a cleanup label like this is often a cleaner solution than the alternatives. If you don't like it, wrapping everything in an if
block works just as well.
Edit
I feel the need to further clarify my earlier statement about having a single return point. With the CHECKER
macro as defined above, the -buildGuts
method can return at any point where that macro is used. If there are any retained objects local to that method, they must be cleaned up before returning. For example, imagine this very reasonable modification to your -buildGuts
method:
-(void)buildGuts
{
// we are actually in the BG here...
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
CHECKER
[self blah blah];
CHECKER
[self blah blah];
CHECKER
[self recordSerialNumberUsingFormatter:formatter];
// ... etc ...
[formatter release];
return;
}
Note that in this case, if the CHECKER
macro causes us to return before the end of the method, then the object in formatter
won't be released and will be leaked. While the [self quickly wrap up in a bow]
call can handle cleanup for any objects reachable through an instance variable or through a global pointer, it cannot release objects that were only available locally in the buildGuts
method. This is why I suggested the goto cleanup
implementation, which would look like this:
#define CHECKER if ( pleaseAbandonYourEfforts == YES ) { goto cleanup; }
-(void)buildGuts
{
// we are actually in the BG here...
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
CHECKER
[self blah blah];
CHECKER
[self blah blah];
CHECKER
[self recordSerialNumberUsingFormatter:formatter];
// ... etc ...
cleanup:
[formatter release];
return;
}
Under this implementation, formatter
will always be released, regardless of when cancellation happens.
In short, whenever you mave a macro that can cause you to return from a method, you need to be very sure that before you prematurely return, all memory management has been taken care of. It's hard to do that cleanly with a macro that causes a return.
Solution 2:
Thanks for the discussion! In my case, I wanted to allow issuing a new async request that would cancel the previous one if it hadn't completed yet. With the example above, I'd have to somehow wait for a signal via the attentionBGIsAllDone
callback that the outstanding request was canceled before I could issue a new request. Instead, I created a simple boolean wrapper, an instance of which I could associate with an outstanding request:
@interface MyMutableBool : NSObject {
BOOL value;
}
@property BOOL value;
@end
@implementation MyMutableBool
@synthesize value;
@end
And use an instance of that for pleaseAbandonYourEfforts
. Before I do my dispatch_async
(i.e. in procedurallyBuildEnormousSpaceship
above), I cancel the old request and prepare for the new one as follows:
// First cancel any old outstanding request.
cancelRequest.value = YES;
// Now create a new flag to signal whether or not to cancel the new request.
MyMutableBool *cancelThisRequest = [[[MyMutableBool alloc] init] autorelease];
self.cancelRequest = cancelThisRequest;
My block performing the async task would have to check cancelThisRequest.value
of course.