2

In my iOS program the following happens: As the user types, a request is fired off to a thread where a database lookup is initiated. When the DB lookup is done, a response is fired off on the main thread so the app can display the results.

This works great, except that if the user types really fast, there may be several requests in-flight. Eventually the system will catch up, but it seems inefficient.

Is there a neat way to implement it so that if a request is initiated, I can detect that a lookup is already in progress, and the request should instead be stored as "potentially newest that superceeds the one in-flight"?

SAMPLE SOLUTION WITH COMMENTS ADDED BELOW

Here's the body of a view controller for a small sample project that illustrates the properties of the solution. When typing you might get output like this:

2012-11-11 11:50:20.595 TestNsOperation[1168:c07] Queueing with 'd'
2012-11-11 11:50:20.899 TestNsOperation[1168:c07] Queueing with 'de'
2012-11-11 11:50:21.147 TestNsOperation[1168:c07] Queueing with 'det'
2012-11-11 11:50:21.371 TestNsOperation[1168:c07] Queueing with 'dett'
2012-11-11 11:50:21.599 TestNsOperation[1168:1b03] Skipped as out of date with 'd'
2012-11-11 11:50:22.605 TestNsOperation[1168:c07] Up to date with 'dett'

In this case, the first enqueued operation is skipped because it determines that it has become outdated while performing the lengthy part of its work. The two following enqueued operations ('de' and 'det') are cancelled before they are even allowed to execute. The final final operation is the only one to actually finish all its work.

If you comment out the [self.lookupQueue cancelAllOperations] line, you get this behavior instead:

2012-11-11 11:55:56.454 TestNsOperation[1221:c07] Queueing with 'd'
2012-11-11 11:55:56.517 TestNsOperation[1221:c07] Queueing with 'de'
2012-11-11 11:55:56.668 TestNsOperation[1221:c07] Queueing with 'det'
2012-11-11 11:55:56.818 TestNsOperation[1221:c07] Queueing with 'dett'
2012-11-11 11:55:56.868 TestNsOperation[1221:c07] Queueing with 'dette'
2012-11-11 11:55:57.458 TestNsOperation[1221:1c03] Skipped as out of date with 'd'
2012-11-11 11:55:58.461 TestNsOperation[1221:4303] Skipped as out of date with 'de'
2012-11-11 11:55:59.464 TestNsOperation[1221:1c03] Skipped as out of date with 'det'
2012-11-11 11:56:00.467 TestNsOperation[1221:4303] Skipped as out of date with 'dett'
2012-11-11 11:56:01.470 TestNsOperation[1221:c07] Up to date with 'dette'

In this case, all the enqueued operation will perform the length part of their work, even though a newer operation has been enqueued after it before it has even been scheduled for execution.

@interface SGPTViewController ()

@property (nonatomic, strong) NSString* oldText;
@property (strong) NSOperationQueue *lookupQueue;

@end

@implementation SGPTViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    self.oldText = self.source.text;
    self.lookupQueue = [[NSOperationQueue alloc] init];
    self.lookupQueue.maxConcurrentOperationCount = 1;
}

- (void)textViewDidChange:(UITextView *)textView
{
    // avoid having a strong reference to self in the operation queue
    SGPTViewController * __weak blockSelf = self;

    // you can cancel existing operations here if you want
    [self.lookupQueue cancelAllOperations];

    NSString *outsideTextAsItWasWhenStarted = [NSString stringWithString:self.source.text];
    NSLog(@"Queueing with '%@'", outsideTextAsItWasWhenStarted);
    [self.lookupQueue addOperationWithBlock:^{
        // do stuff
        NSString *textAsItWasWhenStarted = [NSString stringWithString:outsideTextAsItWasWhenStarted];
        [NSThread sleepForTimeInterval:1.0];
        if (blockSelf.lookupQueue.operationCount == 1) {
            // do more stuff if there is only one operation on the queue,
            // i.e. this one. Operations are removed when they are completed or cancelled.
            // I should be canceled or up to date at this stage
            dispatch_sync(dispatch_get_main_queue(), ^{
                if (![textAsItWasWhenStarted isEqualToString:self.source.text]) {
                    NSLog(@"NOT up to date with '%@'", textAsItWasWhenStarted);
                } else {
                    NSLog(@"Up to date with '%@'", textAsItWasWhenStarted);
                }
            });
        } else {
            NSLog(@"Skipped as out of date with '%@'", textAsItWasWhenStarted);
        }
    }];
}
Anders Johansen
  • 10,165
  • 7
  • 35
  • 52

3 Answers3

5

If your query really takes a long time I would think of mechanism that would slow down query by let's say 1s and cancel previous queries requests if any. So if you were using blocks it might have been something like that:

@interface YourViewController
   @property(assign) NSInteger currentTaskId; // atomic

...

@implementation YourViewController
@synthesize currentTaskId;
// your target method
- (void)textFieldDidChange
{
        self.currentTaskId = self.currentTaskId + 1;
        NSInteger taskId = self.currentTaskId;

        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), queue, ^{
            if (taskId == self.currentTaskId) // this is still current task
            {
                // your query

                if (taskId == self.currentTaskId) // sill current after query? update visual elements
                {
                    // your main thread updates
                }
            } // else - there is newer task so skip this old query 
        });
}
Tomasz Zabłocki
  • 1,326
  • 7
  • 14
  • If you're relying on the `atomic` semantics of the property, it won't work because you're accessing the underlying ivar. Use `self.currentTaskId` instead. That said, even `self.currentTaskId++` is not atomic, so if `currentTaskId` is accessed across multiple threads, and it appears to be, this *will* fail. Look at using `OSAtomicIncrement32Barrier()` with an `int32_t` for proper atomicity. – Jonathan Grynspan Nov 05 '12 at 13:43
  • I am relying on synthesized getter/setter which should be atomic and safe between multiple threads. Do you think it's right? (I forgot about synthesize keyword before). – Tomasz Zabłocki Nov 05 '12 at 14:09
  • ...but the query does not take a long time. Typically it's on the order of < 1/10th of a second. Still, it involves a lot of IO and processing, so I'd like to have it run more efficiently. I'm sure the battery drain will also be reduced significantly. – Anders Johansen Nov 05 '12 at 15:14
  • @TomaszZabłocki: You're not using the getters and setters anywhere in your code. You're using the ivar directly. – Jonathan Grynspan Nov 05 '12 at 15:20
  • 1
    @TomaszZabłocki: And `currentTaskId = currentTaskId + 1` is just as non-atomic as `currentTaskId++`. Neither is an atomic operation and neither uses the accessors. – Jonathan Grynspan Nov 05 '12 at 15:21
  • 1
    Oh I forget about self., corrected that now, but for the second issue I think it's safe, my assumption is that textFieldDidChange will be called from main(UI) thread only. Thanks. – Tomasz Zabłocki Nov 05 '12 at 16:36
  • This solution was successfully used in different question, you can find it [here](http://stackoverflow.com/questions/13277974/ios-call-function-after-a-few-seconds-the-user-did-finish-typing/13278865#13278865) – Tomasz Zabłocki Nov 08 '12 at 15:28
  • assuming you're using dispatch queues, this is a related answer: http://stackoverflow.com/a/1551056/787801 – Sam Hatchett Nov 08 '12 at 22:51
  • Thnks buddy i really want like this thank you so much brother. – Super Developer Nov 24 '16 at 11:13
4

I like NSOperationQueue for situations like this.

@interface ClassName ()
...
// atomic since it does not specify nonatomic
@property (strong) NSOperationQueue *lookupQueue;
...
@end

- (id)init
{
    ...
    lookupQueue = [[NSOperationQueue alloc] init];
    lookupQueue.maxConcurrentOperationCount = 1;
    ...
}

- (void)textFieldDidChange
{
    // avoid having a strong reference to self in the operation queue
    ClassName * __weak blockSelf = self;

    // you can cancel existing operations here if you want
    // [lookupQueue cancelAllOperations];

    [lookupQueue addOperationWithBlock:^{
        // do stuff
        ...
        if (blockSelf.lookupQueue.operationCount == 1) {
            // do more stuff if there is only one operation on the queue,
            // i.e. this one. Operations are removed when they are completed or cancelled.
        }
    }];
}

Edit: Just a note, you need to use [[NSOperationQueue mainQueue] addOperationWithBlock:] or similar to update the GUI or run any other code that must go on the main thread, from inside the block argument to [lookupQueue addOperationWithBlock:].

Brendon
  • 882
  • 6
  • 12
  • OK, this looks neat. I'll just test that it works in my setting, then get back to you - hopefully with an accept for the answer. – Anders Johansen Nov 10 '12 at 05:03
  • I have accepted your answer and awarded the bonus. My sample solution has an attempt in it to access tings on the main thread where I use dispatch_sync(dispatch_get_main_queue(), (...). Will that work just as well as using [[NSOperationQueue mainQueue] addOperationWithBlock:] ? – Anders Johansen Nov 12 '12 at 08:15
  • dispatch_sync() is the best way I know of to synchronously run code on the main thread, from another thread. You should be fine using it. – Brendon Nov 12 '12 at 15:14
1

NSOperationQueue provides a method -cancelAllOperations. So just call that when adding an operation, if an operation is already running. The remainder of the problem is that your NSOperation subclass must periodically check whether it has been cancelled (and stop doin' stuff, in that scenario). That check is placed in your override of -main.

justin
  • 104,054
  • 14
  • 179
  • 226