7

我一直在想,你可以在你用 GCD 启动的线程中使用 cancel/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 进程时,都必须清理您打开的所有变量/上下文/内存。希望它可以帮助某人,有时!

4

2 回答 2

18

GCD 没有内置的取消支持;如果能够取消您的后台任务很重要,那么像您演示的那样检查标志是一个可以接受的解决方案。但是,您可能需要评估取消需要多快做出响应;如果其中一些方法调用相当短,您可能能够减少检查频率。

您问是否可以使用 NSOperation 标志来支持取消。答案是不。GCD 不是基于 NSOperation。事实上,在 Snow Leopard 中,NSOperation 和 NSOperationQueue 被重新实现为在内部使用 GCD。所以依赖是相反的。NSOperation 是比 GCD 更高级别的构造。但是,即使您要使用 NSOperation,您的取消实现也基本相同;你仍然需要self.isCancelled定期检查,看看你是否应该放弃太空船的建造。

我对您实现CHECKER宏的唯一担心是它实现了一个意想不到的return. 因此,您必须小心内存泄漏。如果您在后台线程上设置了自己的 NSAutoreleasePool,则需要drain在返回之前设置它。如果您已经alloced 或retained 任何对象,您可能需要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宏导致我们在方法结束之前返回,那么 in 中的对象formatter将不会被释放并会被泄漏。虽然该[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,无论何时发生取消,都将始终被释放。

简而言之,每当你创建一个可以导致你从一个方法返回的宏时,你需要非常确定在你过早返回之前,所有的内存管理都已经处理好了。使用导致返回的宏很难干净地做到这一点。

于 2011-03-27T13:54:10.423 回答
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

于 2011-11-15T02:42:00.340 回答