Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement an NSRunLoop inside an NSOperation

Im posting this question because I have seen a lot of confusion over this topic and I spent several hours debugging NSOperation subclasses as a result.

The problem is that NSOperation doesnt do you much good when you execute Asynchronous methods which are not actually complete until the asynchronous callback completes.

If the NSOperation itself is the callback delegate it may not even be sufficient to properly complete the operation due to the callback occurring on a different thread.

Lets say you are in the main thread and you create an NSOperation and add it to an NSOperationQueue the code inside the NSOperation fires an Asynchronous call which calls back to some method on the AppDelegate or a view controller.

You cant block the main thread or the UI will lock up, so you have two options.

1) Create an NSOperation and add it to the NSOperationQueue with the following signature:

[NSOperationQueue addOperations:@[myOp] waitUntilFinished:?]

Good luck with that. Asynchronous operations usually require a runloop so it wont work unless you subclass NSOperation or use a block, but even a block wont work if you have to "complete" the NSOperation by telling it when the callback has finished.

So...you subclass NSOperation with something akin to the following so the callback can tell the operation when its finished:

//you create an NSOperation subclass it includes a main method that
//keeps the runloop going as follows
//your NSOperation subclass has a BOOL field called "complete"

-(void) main
{

    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

    //I do some stuff which has async callbacks to the appDelegate or any other class (very common)

    while (!complete && [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);

}

//I also have a setter that the callback method can call on this operation to 
//tell the operation that its done, 
//so it completes, ends the runLoop and ends the operation

-(void) setComplete {
    complete = true;
}

//I override isFinished so that observers can see when Im done
// - since my "complete" field is local to my instance

-(BOOL) isFinished
{
    return complete;
}

OK - This absolutely does not work - we got that out of the way!

2) The second problem with this method is that lets say the above actually worked (which it does not) in cases where runLoops have to terminate properly, (or actually terminate at all from an external method call in a callback)

Lets assume for a second Im in the main thread when I call this, unless I want the UI to lock up for a bit, and not paint anything, I cant say "waitUntilFinished:YES" on the NSOperationQueue addOperation method...

So how do I accomplish the same behavior as waitUntilFinished:YES without locking up the main thread?

Since there are so many questions regarding runLoops, NSOperationQueues and Asynch behavior in Cocoa, I will post my solution as an answer to this question.

Note that Im only answering my own question because I checked meta.stackoverflow and they said this is acceptable and encouraged, I hope the answer that follows helps people to understand why their runloops are locking up in NSOperations and how they can properly complete NSOperations from external callbacks. (Callbacks on other threads)

like image 567
deleted_user Avatar asked Sep 05 '12 15:09

deleted_user


2 Answers

The answer to problem #1

I have an NSOperation which calls an Asynchronous operation in its main method which calls back outside the operation and I need to tell the operation its complete and end the NSOperation:

The following code is amended from above

//you create an NSOperation subclass it includes a main method that
//keeps the runloop going as follows
//your NSOperation subclass has a BOOL field called "complete"
//ADDED: your NSOperation subclass has a BOOL field called "stopRunLoop"
//ADDED: your NSOperation subclass has a NSThread * field called "myThread"
-(void) main
{
    myThread = [NSThread currentThread];
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

    //I do some stuff which has async callbacks to the appDelegate or any other class (very common)

    while (!stopRunLoop && [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);

    //in an NSOperation another thread cannot set complete 
    //even with a method call to the operation
    //this is needed or the thread that actually invoked main and 
    //KVO observation will not see the value change
    //Also you may need to do post processing before setting complete.
    //if you just set complete on the thread anything after the 
    //runloop will not be executed.
    //make sure you are actually done.

    complete = YES;

}


-(void) internalComplete
{
    stopRunloop = YES;
}

//This is needed to stop the runLoop, 
//just setting the value from another thread will not work,
//since the thread that created the NSOperation subclass 
//copied the member fields to the
//stack of the thread that ran the main() method.

-(void) setComplete {
    [self performSelector:@selector(internalComplete) onThread:myThread withObject:nil      waitUntilDone:NO];
}

//override isFinished same as before
-(BOOL) isFinished
{
    return complete;
}

Answer to problem #2 - You cant use

[NSOperationQueue addOperations:.. waitUntilFinished:YES]

Because your main thread will not update, but you also have several OTHER operations which must not execute until this NSOperation is complete, and NONE of them should block the main thread.

Enter...

dispatch_semaphore_t

If you have several dependent NSOperations which you need to launch from the main thread, you can pass a dispatch semaphore to the NSOperation, remember that these are Asynchronous calls inside the NSOperation main method, so the NSOperation subclass needs to wait for those callbacks to complete. Also method chaining from callbacks can be a problem.

By passing in a semaphore from the main thread you can use [NSOperation addOperations:... waitUntilFinished: NO] and still prevent other operations from executing until your callbacks have all completed.

Code for the main thread creating the NSOperation

//only one operation will run at a time
dispatch_semaphore_t mySemaphore = dispatch_semaphore_create(1);

//pass your semaphore into the NSOperation on creation
myOperation = [[YourCustomNSOperation alloc] initWithSemaphore:mySemaphore] autorelease];

//call the operation
[myOperationQueue addOperations:@[myOperation] waitUntilFinished:NO];

...Code for the NSOperation

//In the main method of your Custom NSOperation - (As shown above) add this call before
//your method does anything
//my custom NSOperation subclass has a field of type dispatch_semaphore_t
//named  "mySemaphore"

-(void) main
{
    myThread = [NSThread currentThread];
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

    //grab the semaphore or wait until its available
    dispatch_semaphore_wait(mySemaphore, DISPATCH_TIME_FOREVER);

    //I do some stuff which has async callbacks to the appDelegate or any other class (very common)

    while (!stopRunLoop && [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);

    //release the semaphore
    dispatch_semaphore_signal(mySemaphore);

    complete = YES;

}

When your callback method on another thread calls setComplete on the NSOperation 3 things will happen,

  1. The runloop will be stopped allowing the NSOperation to complete (which it otherwise would not)

  2. The semaphore will be released allowing other operations sharing the semaphore to run

  3. The NSOperation will complete and be dealloced

If you use method 2 you can wait on arbitrary asynchronous methods invoked from an NSOperationQueue, know that they will complete the runloop, and you can chain callbacks in any way you like, while never blocking the main thread.

like image 188
deleted_user Avatar answered Nov 15 '22 19:11

deleted_user


I didn't read these answers in any great detail because these approaches are a) way too complicated and b) not using NSOperation the way it's designed to be used. You guys seem to be hacking functionality that already exists.

The solution is to subclass NSOperation and override the getter isConcurrent to return YES. You then implement the - (void)start method and begin your asynchronous task. You are then responsible for finishing it, meaning you have to generate KVO notifications on isFinished and isExecuting so that the NSOperationQueue can know the task is complete.

(UPDATE: Here's how you would subclass NSOperation) (UPDATE 2: Added how you would handle a NSRunLoop if you have code that requires one when working on a background thread. The Dropbox Core API for example)

// HSConcurrentOperation : NSOperation
#import "HSConcurrentOperation.h"  

@interface HSConcurrentOperation()
{
@protected

    BOOL _isExecuting;
    BOOL _isFinished;

    // if you need run loops (e.g. for libraries with delegate callbacks that require a run loop)
    BOOL _requiresRunLoop;
    NSTimer *_keepAliveTimer;  // a NSRunLoop needs a source input or timer for its run method to do anything.
    BOOL _stopRunLoop;
}
@end

@implementation HSConcurrentOperation

- (instancetype)init
{
    self = [super init];
    if (self) {
        _isExecuting = NO;
        _isFinished = NO;

    }
    return self;
}

- (BOOL)isConcurrent
{
    return YES;
}

- (BOOL)isExecuting
{
    return _isExecuting;
}

- (BOOL)isFinished
{
    return _isFinished;
}

- (void)start
{

    [self willChangeValueForKey:@"isExecuting"];
    NSLog(@"BEGINNING: %@", self.description);
    _isExecuting = YES;
    [self didChangeValueForKey:@"isExecuting"];

    _requiresRunLoop = YES;  // depends on your situation.
    if(_requiresRunLoop)
    {
       NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

       // run loops don't run if they don't have input sources or timers on them.  So we add a timer that we never intend to fire and remove him later.
       _keepAliveTimer = [NSTimer timerWithTimeInterval:CGFLOAT_MAX target:self selector:@selector(timeout:) userInfo:nil repeats:nil];
       [runLoop addTimer:_keepAliveTimer forMode:NSDefaultRunLoopMode];

       [self doWork];

       NSTimeInterval updateInterval = 0.1f;
       NSDate *loopUntil = [NSDate dateWithTimeIntervalSinceNow:updateInterval];
       while (!_stopRunLoop && [runLoop runMode: NSDefaultRunLoopMode beforeDate:loopUntil])
       {
           loopUntil = [NSDate dateWithTimeIntervalSinceNow:updateInterval];
       }

    }
    else
    {
      [self doWork];
    }
}

- (void)timeout:(NSTimer*)timer
{
    // this method should never get called.

    [self finishDoingWork];
}

- (void)doWork
{
    // do whatever stuff you need to do on a background thread.
    // Make network calls, asynchronous stuff, call other methods, etc.

    // and whenever the work is done, success or fail, whatever
    // be sure to call finishDoingWork.

    [self finishDoingWork];
}

- (void)finishDoingWork
{
   if(_requiresRunLoop)
   {
      // this removes (presumably still the only) timer from the NSRunLoop
      [_keepAliveTimer invalidate];
      _keepAliveTimer = nil;

      // and this will kill the while loop in the start method
      _stopRunLoop = YES;
   }

   [self finish];

}
- (void)finish
{
    // generate the KVO necessary for the queue to remove him
    [self willChangeValueForKey:@"isExecuting"];
    [self willChangeValueForKey:@"isFinished"];

    _isExecuting = NO;
    _isFinished = YES;

    [self didChangeValueForKey:@"isExecuting"];
    [self didChangeValueForKey:@"isFinished"];

}

@end
like image 33
horseshoe7 Avatar answered Nov 15 '22 20:11

horseshoe7