codeBearer
codeBearer

Reputation: 5194

NSRunLoop's runMode:beforeDate: - the correct approach for setting the "beforeDate"

I have a doubt regarding the correct usage of NSRunLoop's runMode:beforeDate method.

I have a secondary, background thread that processes delegate messages as they are received.

Basically, I have process intensive logic that needs to be executed on a background thread.

So, I have 2 objects, ObjectA and AnotherObjectB.

ObjectA initializes AnotherObjectB and tells AnotherObjectB to start doing it's thing. AnotherObjectB works asynchronously, so ObjectA acts as AnotherObjectB's delegate. Now, the code that needs to be executed in the delegate messages, needs to be done on a background thread. So, for ObjectA, I created an NSRunLoop, and have done something like this to set the run loop up:

do {
 [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
} while (aCondition);

Where aCondition is set somewhere in the "completion delegate message".

I'm getting all my delegate messages and they are being processed on that background thread.

My question being: is this the correct approach?

The reason I ask this is because [NSDate distantFuture] is a date spanning a couple of centuries. So basically, the runLoop won't timeout until "distantFuture" - I definitely won't be using my Mac or this version of iOS till then. >_<

However, I don't want the run loop to run that long. I want the run loop to get done as soon as my last delegate message is called, so that it can properly exit.

Also, I know that I can set repeating timers, with shorter intervals, but that is not the most efficient way since it's akin to polling. Instead, I want the thread to work only when the delegate messages arrive, and sleep when there are no messages. So, is the approach I'm taking the correct approach, or is there some other way of doing it. I read the docs and the guide, and I set this up based off what I understood from reading them.

However, when not completely sure, best to ask this awesome community for an opinion and confirmation.

So, thanks in advance for all your help!

Cheers!

Upvotes: 3

Views: 4426

Answers (2)

Mecki
Mecki

Reputation: 133219

The reason I ask this is because [NSDate distantFuture] is a date spanning a couple of centuries.

The method runMode:beforeDate: will

  • return NO immediately if there are no sources scheduled on the RunLoop.

  • return YES whenever an event has been processed.

  • return YES when the limitDate has been reached.

So even if the limitDate is very high, it will return after every processed event, it will not keep running until limitDate has been hit. It would only wait for that long if no event is ever processed. limitDate is thus like a timeout after that the method will give up on waiting for an event to take place. But if you want to have multiple events in a row handled, you must call this method over and over again, hence the loop.

Think of fetching packets with timeout from a network socket. The fetch call returns when a packet arrives or when the timeout has been hit. Yet if you want to process the next packet, you must call the fetch method again.

The following is unfortunately pretty bad code for two reasons:

// BAD CODE! DON'T USE!
NSDate * distFuture = NSDate.distantFuture;
NSRunLoop * runLoop = NSRunLoop.currentRunLoop;
while (keepRunning) {
    [runLoop runMode:NSDefaultRunLoopMode beforDate:distFuture];
}
  1. If no RunLoopSource is yet scheduled on the RunLoop, it will waste 100% CPU time, as the method will return at once just to be called again and that as fast as the CPU is able to do so.

  2. The AutoreleasePool is never renewed. Objects that are autoreleased (and even ARC does that) are added to the current pool but are never released as the pool is never cleared, so memory consumption will raise as long as this loop is running. How much depends on what your RunLoopSources are actually doing and how they are doing it.

A better version would be:

// USE THIS INSTEAD
NSDate * distFuture = NSDate.distantFuture;
NSRunLoop * runLoop = NSRunLoop.currentRunLoop;
while (keepRunning) @autoreleasepool {
    BOOL didRun = [runLoop runMode:NSDefaultRunLoopMode beforDate:distFuture];
    if (!didRun) usleep(1000);
}

It solves both problems:

  • An AutoreleasePool is created the first time the loop runs and after every run it is cleared, so memory consumption will not raise over time.

  • In case the RunLoop didn't really run at all, the current thread sleeps for one millisecond before trying again. This way the CPU load will be pretty low since as as no RunLoopSource is set, this code only runs once every millisecond.

To reliably terminate the loop, you need to do two things:

  1. Set keepRunning to NO. Note that you must declare keepRunning as volatile! If you don't do that, the compiler may optimize the check away and turn your loop into an endless loop since it sees no code in the current execution context that would ever change the variable and it cannot know that some other code somewhere else (and maybe on another thread) may change it in the background. This is why you usually need a memory barrier for these cases (a lock, a mutex, a semaphore, or an atomic operation), as compilers don't optimize across those barriers. However, in that simple case, using volatile is enough, as BOOL is always atomic in Obj-C and volatile tells the compiler "Always check thes value of this variable as it may change behind your back without you seeing that change at compile time".

  2. If the variable has been changed from another thread and not from within an event handler, your RunLoop thread may be sleeping inside the runMode:beforeDate: call, waiting for a RunLoopSource event to take place which may take any amount of time or never happen at all anymore. To force this call to return immediately, just schedule an event after changing the variable. This can be done with performSelector:onThread:withObject:waitUntilDone: as shown below. Performing this selector counts as a RunLoop event and the method will return after the selector was called, see that the variable has changed and break out of the loop.

volatile BOOL keepRunning;

- (void)wakeMeUpBeforeYouGoGo {
    // Jitterbug
}

// ... In a Galaxy Far, Far Away ...
    keepRunning = NO;
    [self performSelector:@selector(wakeMeUpBeforeYouGoGo) 
        onThread:runLoopThread withObject:nil waitUntilDone:NO];

Upvotes: 2

tc.
tc.

Reputation: 33602

The code is in the docs:

If you want the run loop to terminate, you shouldn't use this method. Instead, use one of the other run methods and also check other arbitrary conditions of your own, in a loop. A simple example would be:

BOOL shouldKeepRunning = YES;        // global
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);

where shouldKeepRunning is set to NO somewhere else in the program.

After your last "message", un-set shouldKeepRunning (on the same thread as the run loop!) and it should finish. The key idea here is that you need to send the run loop an event so it knows to stop.

(Also note that NSRunLoop is not thread-safe; I think you're supposed to use -[NSObject performSelector:onThread:...].)

Alternatively, if it works for your purposes, use a background a dispatch queue/NOperationQueue (but note that code which does this shouldn't touch the run loop; things like starting a NSURLConnection from a dispatch queue/NSOperationQueue worker thread will likely cause problems).

Upvotes: 3

Related Questions