views:

891

answers:

3

Hi all,

I was just recently futzing about with a sample application trying to get my head completely wrapped around NSRunLoop. The sample I wrote created a simple secondary thread via NSOperation. The secondary thread does a few tasks such as process an NSTimer and also some rudimentary streaming with NSStream. Both of these input sources require a properly configured NSRunLoop in order to execute.

My question is this. Originally I had some code that looked like this in the secondary thread:

NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
self.connectTimer = [NSTimer scheduledTimerWithTimeInterval:connectTimeout
                                                     target:self
                                                   selector:@selector(connectionConnectedCheck:)
                                                   userInfo:nil 
                                                    repeats:NO];

[myRunLoop addTimer:self.connectTimer forMode:NSDefaultRunLoopMode]; // added source here
[myRunLoop run];

[NSStream getStreamsToHostNamed:relayHost port:relayPort inputStream:&inputStream outputStream:&outputStream];
if ((inputStream != nil) && (outputStream != nil))
{
    sendState = kSKPSMTPConnecting;
    isSecure = NO;

    [inputStream retain];
    [outputStream retain];

    [inputStream setDelegate:self];
    [outputStream setDelegate:self];

    [inputStream scheduleInRunLoop: myRunLoop //[NSRunLoop currentRunLoop] 
         forMode:NSRunLoopCommonModes];
    [outputStream scheduleInRunLoop: myRunLoop //[NSRunLoop currentRunLoop] 
         forMode:NSRunLoopCommonModes];

    [inputStream open];
    [outputStream open];

    self.inputString = [NSMutableString string];



    return YES;
}

Now, with the code above, the events would never process. Not on currentRunLoop. I did something terrible afterwards, as this was just an educational exercise, and modified it to run under NSRunLoop mainRunLoop. Worked like magic. However, I'm almost certain that depending on my main threads run loop in a secondary thread is on 10 different levels of wrong.

So my question is in two parts, and I hope that's OK.

  1. What can possibly go wrong with the little 'hack' I applied in order to get the secondary thread to run and respond to events via the run loop?

  2. What's the proper way to configure the secondary thread to listen to all events/timer based sources so I don't have to do step 1.

Thanks for the insight all.

+2  A: 

Did you remember to run the runloop?

[[NSRunLoop currentLoop] run]
Dietrich Epp
I have indeed tried that. No luck. Code will only execute if ran with [NSRunLoop mainRunLoop]
A: 

Answering your questions in reverse order:

2. You've got two problems. The documentation for -[NSRunLoop run] says:

If no input sources or timers are attached to the run loop, this method exits immediately; otherwise, it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate:. In other words, this method effectively begins an infinite loop that processes data from the run loop’s input sources and timers.

So using your thread's own run loop, there are probably not input sources defined for the run loop, so it is returning immediately. If there are, your run loop is looping infinitely, and the rest of your code after that point is never being executed.

In order to get things to work correctly, your run loop needs to first have some input sources, and then needs to run periodically to check for events. Note that you don't want to use [NSRunLoop run], as you'll never get control back. Instead, I would recommend setting up a loop right before return YES that continually runs the run loop until the thread has been cancelled, or until you're done streaming data. That will allow the run loop to process data as it arrives. Something like this:

while (!done) {
    [myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
}

[NSRunLoop runUntilDate:] processes events until the specified date, and then returns control to your program so that you can do whatever you want.

1. It's working because your main thread's run loop is being run periodically, so events are being handled. If your main thread ever blocked, however, your data would stop arriving. This could be particularly bad if the main thread was waiting for data from your second thread. Also, NSRunLoop is not thread-safe:

Warning: The NSRunLoop class is generally not considered to be thread-safe and its methods should only be called within the context of the current thread. You should never try to call the methods of an NSRunLoop object running in a different thread, as doing so might cause unexpected results. (from the NSRunLoop documentation.)

Apple's Threading Programming Guide has a section entitled Run Loop Management, which explains all of this to some degree. It's not the clearest document I've ever read, but if you're working with run loops, it's a good place to start.

BJ Homer
+1  A: 

First off, the scheduledTimer... method is supposed to add the timer to the current run loop automatically. You only need to use the addTimer: method if you created the timer using the initWithFireDate... initializer. I doubt adding the timer twice would cause a problem, but it's a possibility.

The run method of NSRunLoop is not supposed to return until there are no more event sources or the loop is explicitly exited. That means you need to schedule any event sources before calling run. Move your [myRunLoop run] call to the very end of your code sample. (Also, obviously, eliminate the return statement)

The bottom line is that if [NSRunLoop run] is returning, then you don't have an event source scheduled on it. Use the debugger to step through the code. If you see it progress past the [NSRunLoop run] call, you can be sure you have a problem with your input sources.

Alex