Speckpgh
Speckpgh

Reputation: 3372

iOS UI Thread/Background or Background to Background thread communication

I have a map based program, that when the user interacts with a element on the screen the app goes and queries the database and adds annotations based on what they user selects. To prevent the entire map UI from locking up, I have put this query and add annotation code in a background thread.. and it works perfectly.

Now the issue is, if the user clicks on another element on the interface, before the background thread finishes a new thread is launched to do the same thing. This in and over itself isn't a problem per se, but on slower devices (old iPods and iphone 3gs etc) it is possible that the second thread finishes before the first one, so the user briefly gets the view related to the last interaction, but then is shown the result of the first one, if the first interaction takes a long time to process.. (classic racing condition).

So, what I would like to do, is have the second interaction, inform any background thread already in flight that hey, you can basically quit what you are doing now and end. How would I go about doing this?

Upvotes: 1

Views: 3263

Answers (3)

Jody Hagins
Jody Hagins

Reputation: 28419

Use a GCD dispatch queue. It automatically provides FIFO ordering, and blocks. Although you can not cancel a request that is already running, you can prevent others from running.

Since you did not say you were using CoreData, and you are already using a separate thread, I'll assume you are NOT using CoreData as your "database."

Try something simple like...

Create your queue when your class initializes (or app starts if its always supposed to be there)...

dispatch_queue_t workQ = dispatch_queue_create("label for your queue", 0);

and release it when your class deallocs (or other appropriate time)...

dispatch_release(workQ);

To get a semblance of cancels, if another selection has been made, you can do something simple like using a token to see if the request you are working on is the "latest" or whether another request has come along since then...

static unsigned theWorkToken;
unsigned currentWorkToken = ++theWorkToken;
dispatch_async(workQ, ^{
    // Check the current work token at each step, to see if we should abort...
    if (currentWorkToken != theWorkToken) return;
    MyData *data = queryTheDatabaseForData(someQueryCriteria);

    if (currentWorkToken != theWorkToken) return;
    [data doTimeConsumingProcessing];

    for (Foo *foo in [data foo]) {
        if (currentWorkToken != theWorkToken) return;
        // Process some foo object
    }

    // Now, when ready to interact with the UI...
    if (currentWorkToken != theWorkToken) return;
    dispatch_async(dispatch_get_main_queue(), ^{
        // Now you are running in the main thread... do anything you want with the GUI.
    });
});

The block will "capture" the stack variable "currentWorkToken" and its current value. Note, that the unlocked check is fine here, because you do not need to keep track of the continuous count, just if it has changed since you set it. In the worst case, you will do an extra step.

If you are using CoreData, you can create your MOC with NSPrivateQueueConcurrencyType, and now you don't need to create a work queue at all because the private MOC has its own...

static unsigned theWorkToken;
unsigned currentWorkToken = ++theWorkToken;
[managedObjectContext performBlock:^{
    // Check the current work token at each step, to see if we should abort...
    if (currentWorkToken != theWorkToken) return;
    MyData *data = queryTheDatabaseForData(someQueryCriteria);

    if (currentWorkToken != theWorkToken) return;
    [data doTimeConsumingProcessing];

    for (Foo *foo in [data foo]) {
        if (currentWorkToken != theWorkToken) return;
        // Process some foo object
    }

    // Now, when ready to interact with the UI...
    if (currentWorkToken != theWorkToken) return;
    dispatch_async(dispatch_get_main_queue(), ^{
        // Now you are running in the main thread... do anything you want with the GUI.
    });
}];

GCD/Blocks is really the preferred method for this kind of stuff.

EDIT

Why is it preferred? Well, first off, using blocks allows you to keep your code localized, instead of spread out into other methods of yet another class (NSOperation). There are many other reasons, but I'll put my personal reasons aside, because I was not talking about my personal preferences. I was talking about Apple's.

Just sit back one weekend and watch all the WWDC 2011 videos. Go ahead, it's really a blast. I mean that with all sincerity. If you are in the USA, you've got a long weekend coming up. I bet you can't think of something better to do...

Anyway, watch those videos and see if you can count the number of times the different presenters said that they strongly recommend using GCD... and blocks... (and Instruments as another aside).

Now, it may all change really soon with WWDC 2012, but I doubt it.

Specifically, in this case, it's also a better solution than NSOperation. Yes, you can cancel an NSOperation that has not yet started. Whoopee. NSOperation still does not provide an automatic way to cancel an operation that has already begun execution. You have to keep checking isCanceled, and abort when the cancel request has been made. So, all those if (currentToken != theWorkToken) will still have to be there as if ([self isCancelled]). Yes, the latter is easier to read, but you also have to explicitly cancel the outstanding operation(s).

To me, the GCD/blocks solution is much easier to follow, is localized, and (as presented) has implicit cancel semantics. The only advantage NSOperation has is that it will automatically prevent the queued operation from running if it was canceled before it started. However, you still have to provide your own cancel-a-running-operation functionality, so I see no benefit in NSOperation.

NSOperation has its place, and I can immediately think of several places where I would favor it over straight GDC, but for most cases, and especially this case, it's not appropriate.

Upvotes: 2

Mina Nabil
Mina Nabil

Reputation: 676

-[NSOperationQueue cancelAllOperations] calls the -[NSOperation cancel] method, which causes subsequent calls to -[NSOperation isCancelled] to return YES. However, you have done two things to make this ineffective.

You are using @synthesize isCancelled to override NSOperation's -isCancelled method. There is no reason to do this. NSOperation already implements -isCancelled in a perfectly acceptable manner.

You are checking your own _isCancelled instance variable to determine whether the operation has been cancelled. NSOperation guarantees that [self isCancelled] will return YES if the operation has been cancelled. It does not guarantee that your custom setter method will be called, nor that your own instance variable is up to date. You should be checking [self isCancelled]

// MyOperation.h
@interface MyOperation : NSOperation {
}
@end

And the implementation:

// MyOperation.m
@implementation MyOperation

- (void)main {
    if ([self isCancelled]) {
        NSLog(@"** operation cancelled **");
    }
    sleep(1);

    if ([self isCancelled]) {
        NSLog(@"** operation cancelled **");
    }

    // If you need to update some UI when the operation is complete, do this:
    [self performSelectorOnMainThread:@selector(updateButton) withObject:nil waitUntilDone:NO];

    NSLog(@"Operation finished");
}

- (void)updateButton {
    // Update the button here
}
@end

Note that you do not need to do anything with isExecuting, isCancelled, or isFinished. Those are all handled automatically for you. Simply override the -main method. It's that easy.

Then create object from MyOperation class and and operation to the queue when there is another operation .. you can check if you have operations already running or not --?? if you have simply cancel them and make your new operation..

you can read this reference Subclassing NSOperation to be concurrent and cancellable

Upvotes: -1

Joel
Joel

Reputation: 16134

I would put the background activity into an operation (which will run on a background thread), keep an array of what active operations are working on, then when the user hits the map scan the array to see if the same task is already in progress, and if so either don't start the new operation or cancel the current operation. In the operation code you'll want to do regular checks for isCancelled.

Upvotes: 1

Related Questions