Can you use cancel/isCancelled with GCD/dispatch_async?

匆匆过客 提交于 2019-11-27 04:01:25

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 alloced or retained 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.

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.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!