1

I have recently become thread curious on iOS. Please point me in the direction you would take, to achieve (if possible) the following on modern iOS devices... thank you!

The user is typing in text, say a word every few seconds.

From time to time I want to launch DifficultProcess to do some semantic processing. In short, I guess I need to be able to do four things:

  • launch DifficultProcess from main
  • if DifficultProcess completes, get a message back from it to the same main
  • abandon, get rid of, DifficultProcess if I want to, from main
  • and finally the priority question: DifficultProcess must have much lower priority than main or user input, I want DifficultProcess to have really really looow priority; is that even possible?

What, essentially, are the calls one uses for A, B, C in modern (2011) (late January) iOS? I don't care about Dad's methods! And is "D" even possible in any way?

I guess those are the four ideas!

So in particular I want to send a message to, in other words call a routine in, the running background process (in that way, one could kill off the running background process if desired, or perhaps change it's mode of operation etc).

(For anyone born before 1997, you will recognise that as a typical "speculative processing" paradigm.)

Thanks for pointers for anyone who can be bothered on this!

Fattie
  • 27,874
  • 70
  • 431
  • 719

3 Answers3

7

I would recommend using NSOperation and NSOperationQueue to manage background activity that you need to be able to cancel arbitrarily.

NSOperation's -cancel and NSOperationQueue's -cancelAllOperations are the methods to look at.

To get messages back from the background to the main thread, the dispatch_async-to-main-thread-queue technique is fine. You can combine this with a delegate protocol for your NSOperation to codify the messages you want to send back.

E.g.

@protocol MyOperationDelegate
- (void) operationStarted:(MyOperation *)operation;
- (void) makingProgressOnItem:(id)anItem otherInterestingItem:(NSDictionary *)otherItem remainingCount:(NSUInteger)count;
- (void) operationWillFinish:(MyOperation *)operation;
@end

@interface MyOperation
id <MyOperationDelegate> delegate;
@end

@implementation MyOperation
...

- (void) cancel
{ 
    [super cancel];

    // Tell the delegate we're about to finish (due to cancellation).
    dispatch_sync (dispatch_get_main_queue(), ^{
      [self.delegate operationWillFinish:self];
    });
}

- (void) main 
{
    // Check for cancellation

    if (self.isCancelled) return;

    // Starting

    dispatch_sync (dispatch_get_main_queue(), ^{
        [self.delegate operationStarted:self];
    });

    if (self.isCancelled) return; // Another cancel check


    // Send async progress messages periodically while doing some work

    while (workNotDone) 
    {
        // Do some work ...

        dispatch_async (dispatch_get_main_queue(), ^{
           [self.delegate makingProgressOnItem:foo otherInterestingItem:bar remainingCount:baz];
        });

       if (self.isCancelled) return;
    }


    // About to finish

    if (!self.isCancelled) {
        dispatch_sync (dispatch_get_main_queue(), ^{
           [self.delegate operationWillFinish:self];
        });
    }
}
@end

KVO is no good for interthread communication; the observation is received on the thread that originates the key value change. So, if your background thread changes a value, your background thread is going to receive the KVO about it. Probably not what you want.

Grandpa's -performSelectorOnMainThread:withObject:waitUntilDone: continues to be a fine way to get messages back to the main thread. The limitation is that your messages can only access one object-based argument. The dispatch_async to the main thread doesn't have this limitation.

If you want to fire off an asynchronous (or synchronous) NSNotification's from a background thread to the main thread, you need to use -performSelectorOnMainThread.

NSNotification *note = [NSNotification notificationWithName:FinishedABunchOfWorkNotification object:self userInfo:nil];
[[NSNotificationCenter defaultCenter] performSelectorOnMainThread:@selector(postNotification:) withObject:note waitUntilDone:YES];
Bill Garrison
  • 2,039
  • 15
  • 18
  • There's a couple of approaches: 1) in main thread code, keep an explicit reference to the NSOperation, and just invoke -cancel on it. Or 2) keep a reference to your NSOperationQueue and invoke -cancelAllOperations on that (which will send -cancel to all operations on the queue). – Bill Garrison Jan 20 '11 at 06:14
  • Apple docs on NSOperation will reveal all. You can set the priority level on an operation. – Bill Garrison Jan 20 '11 at 06:37
3

I would suggest using dispatch_async to the global low priority queue (dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0)).

Cancellation is trickier though. There's no good general mechanism for canceling background work that I'm aware of aside from "chunking" it and checking a flag each chunk

To get messages back just dispatch_async back to the main queue. If you squint just right you can think of dispatch_async as "send message" in an actor model.

(edit) if you need serialization of stuff in the background, make a private queue and set its target to the global low priority one, iirc.

Catfish_Man
  • 41,261
  • 11
  • 67
  • 84
  • So, you can't interrupt a function running on a background thread. What you can do is divide your work up into chunks: __block dispatch_block_t chunk; chunk = ^{ doChunkOfBackgroundWork(); dispatch_async(myQueue, chunk); }; dispatch_async(myQueue, chunk); You can then stick stuff on the background thread between chunks of work by just dispatch_[a]sync()ing to the same queue. For example it could set a variable that the work chunk then accessed. – Catfish_Man Jan 20 '11 at 07:38
2

At the risk of quoting Dad's method (it has been around since iPhone version 2) I use

- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg

It's easy and foolproof as long as you remember that you must create a new autorelease pool in the method you pass as selector, and drain it at the end of the method. Apart from that do whatever you like - EXCEPT touch UIKit. It isn't thread-safe so any UI changes must be done through

- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait

or KVO triggers. Key Value Observing would be a good way for your background thread to communicate to your main thread that the work is done.

- (void)myBackgroundThreadMethod {

    NSAutoreleasePool *threadPool = [[NSAutoreleasePool alloc] init];

    // my time-consuming processing here

    [threadPool drain];
}

For more precise control of threads you need to look at NSThread. Threading Programming Guide lays it all out in detail - if you create a thread through NSThread then you have control over when the thread is started. The document does recommend leaving the thread alone and just letting it terminate - but shows how you can terminate it. One way is - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait

NSThread docs also say "leave priority alone". You can set thread priority with

+ (BOOL)setThreadPriority:(double)priority

but I've never known it to be necessary, the scheduler is smart enough to maintain UI responsiveness.

Adam Eberbach
  • 12,309
  • 6
  • 62
  • 114
  • GCD is perfect for this kind of work. I'd have a timer that gets reset every time the user types something. So when it fires ( in that the user hasn't pressed a key in say 2 seconds ) it calls dispatch_async which fires off the processing and sends a NSNotification to the main thread when its done. – Tony Million Jan 19 '11 at 23:38
  • 1
    Are you sure the observer gets notified on the main thread? I haven't tested that on iOS, but IIRC, in Mac OS X, the observation happens on whatever thread set off the change. – Peter Hosey Jan 20 '11 at 15:49
  • I haven't been at all clear - you can do your UI stuff either in the method you call with performSelectorOnMainThread or you can trigger it via KVO from that method on the main thread. I think you're right about the notification happening on the changing thread. – Adam Eberbach Jan 20 '11 at 18:21