你可以在GCD / dispatch_async中使用cancel / isCancelled吗?

时间:2011-03-27 13:20:41

标签: iphone multithreading ios nsoperation grand-central-dispatch

我一直想知道,您是否可以使用您使用GCD启动的线程取消/ cancelAllOperations / .isCancelled?

目前,我只是使用布尔值作为标志来取消后台进程。

假设您希望在后台进行大量处理,同时保持UI响应,以便您可以捕获取消按钮(或动画显示处理器工作的动画)。我们就是这样做的......

@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;
    }

和CHECKER只是检查它是否为真......

#define CHECKER if ( pleaseAbandonYourEfforts == YES ) \
{NSLog(@"Amazing Interruption System Working!");return;}

这一切都很完美。

但........ 是否可以在这种GCD使用时使用cancel / cancelAllOperations / .isCancelled?

这里的故事是什么?欢呼声。


PS - 适用于使用此“六部分”背景模板的任何初学者。

请注意,正如下面的BJ所述,每当你突破bg进程时......

你必须清理你打开的任何变量!

在我的习语中,你必须分配所有变量,上下文,内存等,特别是在“setUpHere”中。你必须在“wrapUpH​​ere”中释放它们。 (如果你在BG中越来越深入,这个成语继续有效。)

或者,正如BJ在他的例子中所展示的那样。 (如果你使用BJ的方法,如果你更深入,要小心。)

无论使用何种方法,当您突破BG流程时,必须清理已打开的所有变量/上下文/内存。希望它有时可以帮到某人!

2 个答案:

答案 0 :(得分:18)

GCD没有内置的取消支持;如果能够取消你的后台任务很重要,那么检查你所展示的标志是一个可以接受的解决方案。但是,您可能想要评估取消需要响应的速度;如果这些方法中的一些调用相当短,那么你可以不那么频繁地检查。

您询问是否可以使用NSOperation标志来支持取消。答案是不。 GCD不是基于NSOperation。事实上,在Snow Leopard中,NSOperation和NSOperationQueue被重新实施以在内部使用GCD。所以依赖是另一种方式。 NSOperation是一个比GCD更高层次的构造。但是,即使你要使用NSOperation,你的取消实现也会大致相同;你还需要定期检查self.isCancelled,看看你是否应该放弃太空船的建造。

我对CHECKER宏的实现唯一关注的是它实现了一个意外的return。因此,您必须小心内存泄漏。如果您在后台线程上设置了自己的NSAutoreleasePool,则在返回之前需要drain它。如果您alloc编辑或retain编辑任何对象,则可能需要release才能返回。

由于所有清理工作都需要在每次检查时进行,因此您可能需要考虑转向单一返回点。一种方法是将每个方法调用包装在if (pleaseAbandonYourEfforts == NO) { }块中。这可以让您在请求取消后快速进入方法的最后,并将清理保存在一个位置。另一个选项,虽然有些人可能不喜欢它,但是会使宏调用goto cleanup;并在方法结尾附近定义一个cleanup:标签,在那里释放任何需要释放的东西。有些人不喜欢以近乎宗教的方式使用goto,但我发现向这样的清理标签向前跳转通常比替代方案更清晰。如果您不喜欢它,将所有内容包装在if块中也同样适用。


修改

我觉得有必要进一步澄清我之前关于单一回归点的陈述。使用上面定义的CHECKER宏,-buildGuts方法可以在使用该宏的任何位置返回。如果该方法存在任何本地保留对象,则必须在返回之前清除它们。例如,想象一下对-buildGuts方法的这种非常合理的修改:

-(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;
}

请注意,在这种情况下,如果CHECKER宏导致我们在方法结束前返回,则formatter中的对象将不会被释放,将被泄露< / strong>即可。虽然[self quickly wrap up in a bow]调用可以处理通过实例变量或全局指针可到达的任何对象的清理,但它不能释放仅在buildGuts方法中本地可用的对象。这就是我建议goto cleanup实现的原因,它看起来像这样:

#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;
}

在此实施过程中,无论何时取消,都将始终释放formatter

简而言之,每当你制作一个可能导致你从某个方法返回的宏时,你需要非常确定在你过早返回之前,所有的内存管理都已经得到了解决。使用导致返回的宏来干净利落是很难的。

答案 1 :(得分:1)

感谢您的讨论!在我的情况下,我想允许发出一个新的异步请求,如果尚未完成,将取消之前的异步请求。通过上面的示例,我必须以某种方式等待通过attentionBGIsAllDone回调的信号,在我发出新请求之前未完成的请求被取消。相反,我创建了一个简单的布尔包装器,我可以将其与一个未完成的请求相关联:

@interface MyMutableBool : NSObject {
    BOOL value;
}
@property BOOL value;
@end

@implementation MyMutableBool
@synthesize value;
@end

并为pleaseAbandonYourEfforts使用该实例。在我执行dispatch_async之前(即上面的procedurallyBuildEnormousSpaceship),我取消旧请求并准备新请求,如下所示:

// 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;

我执行异步任务的块当然必须检查cancelThisRequest.value