1

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?

Speckpgh
  • 3,332
  • 1
  • 28
  • 46
  • so you need to make the second thread inform first one to quit and then second thread begin?? right – Mina Nabil May 22 '12 at 14:16
  • Bascially yes, if the user interacts with another element on the screen, I need to tell the previous background thread, quit what you are doing and die. Then launch the new thread. – Speckpgh May 22 '12 at 14:23
  • you can use NSoperationQueue and you can add operations to the queue when add the second operation you can check if there is operations in the queue then cancel operations and execute the one you are working with – Mina Nabil May 22 '12 at 15:05
  • i have posted a solution you can check it and update me if you have any questions.. – Mina Nabil May 22 '12 at 15:12

3 Answers3

2

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.

Jody Hagins
  • 27,943
  • 6
  • 58
  • 87
  • Why is GCD the "preferred method"? Best practice is generally the highest level of abstraction, unless lower level functionality is needed for performance issues. In this case, NSOperation (which is built on GCD) is the appropriate abstraction level and provides the built-in features he is looking for (cancel). This is a whole lot of unnecessary complexity for a beginner, without any true upside. – Joel May 22 '12 at 16:23
  • While I appreciate your personal preferences in these trade-off decisions (and disagree with your conclusion), I would hardly call NSOperation "not appropriate". Frankly the differences are at the margins from any objective viewpoint. – Joel May 22 '12 at 17:29
1

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.

Joel
  • 15,654
  • 5
  • 37
  • 60
  • So, you are suggesting, simply referencing the started thread, making sure its not already cancelled, and then calling cancel on it if it is not, and within the thread itself check the isCancelled within the thread loop and behave accordingly if it is? – Speckpgh May 22 '12 at 14:39
  • Basically yes. Except you are referencing the NSOperation, not the thread itself. – Joel May 22 '12 at 14:58
  • Okay, I'm a bit curious why you are suggesting to wrap it in an operation, since NSThread itself does indeed have a cancel method as well? – Speckpgh May 22 '12 at 15:02
  • There's a bunch of reasons. Easier to manage and cancel, better performance, etc. The whole purpose of NSOperation is to take care of all the mgmt overhead for you. http://stackoverflow.com/questions/3041837/nsthread-vs-nsoperationqueue-vs-on-the-iphone – Joel May 22 '12 at 15:59
-1

-[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

Community
  • 1
  • 1
Mina Nabil
  • 676
  • 6
  • 19
  • Did you cut and paste this answer from somewhere else? It doesn't make any sense telling him what he is doing wrong since you haven't seen any sample code. – Joel May 22 '12 at 15:57
  • -1 for cutting and pasting a portion of someone else's answer to a different question (without providing the link). The text you copied doesn't even make sense for this question and the formatting is wrong on top of that. – Joel May 22 '12 at 16:45