Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
339 views
in Technique[技术] by (71.8m points)

iphone - 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!

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...