views:

5482

answers:

6

In the Apple documentation for NSRunLoop there is sample code demonstrating suspending execution while waiting for a flag to be set by something else.

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

I have been using this and it works but in investigating a performance issue I tracked it down to this piece of code. I use almost exactly the same piece of code (just the name of the flag is different :) and if I put a NSLog on the line after the flag is being set (in another method) and then a line after the while() there is a seemingly random wait between the two log statements of several seconds.

The delay does not seem to be different on slower or faster machines but does vary from run to run being at least a couple of seconds and up to 10 seconds.

I have worked around this issue with the following code but it does not seem right that the original code doesn't work.

NSDate *loopUntil = [NSDate dateWithTimeIntervalSinceNow:0.1];
while (webViewIsLoading && [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate:loopUntil])
  loopUntil = [NSDate dateWithTimeIntervalSinceNow:0.1];

using this code, the log statements when setting the flag and after the while loop are now consistently less than 0.1 seconds apart.

Anyone any ideas why the original code exhibits this behaviour?

+4  A: 

Runloops can be a bit of a magic box where stuff just happens.

Basically you're telling the runloop to go process some events and then return. OR return if it doesn't process any events before the timeout is hit.

With 0.1 second timeout, you're htting the timeout more often than not. The runloop fires, doesn't process any events and returns in 0.1 of second. Occasionally it'll get a chance to process an event.

With your distantFuture timeout, the runloop will wait foreever until it processes an event. So when it returns to you, it has just processed an event of some kind.

A short timeout value will consume considerably more CPU than the infinite timeout but there are good reasons for using a short timeout, for example if you want to terminate the process/thread the runloop is running in. You'll probably want the runloop to notice that a flag has changed and that it needs to bail out ASAP.

You might want to play around with runloop observers so you can see exactly what the runloop is doing.

See this Apple doc for more information.

schwa
+1  A: 

I’ve had similar issues while trying to manage NSRunLoops. The discussion for runMode:beforeDate: on the class references page says:

If no input sources or timers are attached to the run loop, this method exits immediately; otherwise, it returns after either the first input source is processed or limitDate is reached. Manually removing all known input sources and timers from the run loop is not a guarantee that the run loop will exit. Mac OS X may install and remove additional input sources as needed to process requests targeted at the receiver’s thread. Those sources could therefore prevent the run loop from exiting.

My best guess is that an input source is attached to your NSRunLoop, perhaps by OS X itself, and that runMode:beforeDate: is blocking until that input source either has some input processed, or is removed. In your case it was taking "couple of seconds and up to 10 seconds" for this to happen, at which point runMode:beforeDate: would return with a boolean, the while() would run again, it would detect that shouldKeepRunning has been set to NO, and the loop would terminate.

With your refinement the runMode:beforeDate: will return within 0.1 seconds, regardless of whether or not it has attached input sources or has processed any input. It's an educated guess (I'm not an expert on the run loop internals), but think your refinement is the right way to handle the situation.

Jon Shea
+5  A: 

If you want to be able to set your flag variable and have the run loop immediately notice, just use -[NSRunLoop performSelector:target:argument:order:modes: to ask the run loop to invoke the method that sets the flag to false. This will cause your run loop to spin immediately, the method to be invoked, and then the flag will be checked.

Chris Hanson
Thanks Chris, unfortunately this won't work as the flag is being set in a delegate method on WebView so I can not just call it immediately.
Dave Verwer
+2  A: 

At your code the current thread will check for the variable to have changed every 0.1 seconds. In the Apple code example, changing the variable will not have any effect. The runloop will run till it processes some event. If the value of weibViewIsLoading has changed, no event is generated automatically, thus it will stay in the loop, why would it break out of it? It will stay there, till it gets some other event to process, then it will break out of it. This may happen in 1, 3, 5, 10 or even 20 seconds. And until that happens, it will not break out of the runloop and thus it won't notice that this variable has changed. IOW the Apple code you quoted is indeterministic. This example will only work if the value change of webViewIsLoading also creates an event that causes the runloop to wake up and this seems not to be the case (or at least not always).

I think you should re-think the problem. Since your variable is named webViewIsLoading, do you wait for a webpage to be loaded? Are you using Webkit for that? I doubt you need such a variable at all, nor any of the code you have posted. Instead you should code your app asynchronously. You should start the "web page load process" and then go back to the main loop and as soon as the page finished loading, you should asynchronously post a notification that is processed within the main thread and runs the code that should run as soon as loading has finished.

Mecki
This is a great explanation of why the RunLoop was not breaking out, Thanks. There is a good reason why the WebView loading needs to be modal though and I need to keep it like this.
Dave Verwer
+1  A: 

Okay, I explained you the problem, here's a possible solution:

@implementation MyWindowController

volatile BOOL pageStillLoading;

- (void) runInBackground:(id)arg
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

    // Simmulate web page loading
    sleep(5);

    // This will not wake up the runloop on main thread!
    pageStillLoading = NO;

    // Wake up the main thread from the runloop
    [self performSelectorOnMainThread:@selector(wakeUpMainThreadRunloop:) withObject:nil waitUntilDone:NO];

    [pool release];
}


- (void) wakeUpMainThreadRunloop:(id)arg
{
    // This method is executed on main thread!
    // It doesn't need to do anything actually, just having it run will
    // make sure the main thread stops running the runloop
}


- (IBAction)start:(id)sender
{
    pageStillLoading = YES;
    [NSThread detachNewThreadSelector:@selector(runInBackground:) toTarget:self withObject:nil];
    [progress setHidden:NO];
    while (pageStillLoading) {
     [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }
    [progress setHidden:YES];
}

@end

start displays a progress indicator and captures the main thread in an internal runloop. It will stay there till the other thread announces that it is done. To wake up the main thread, it will make it process a function with no purpose other than waking the main thread up.

This is just one way how you can do it. A notification being posted and processed on main thread might be preferable (also other threads could register for it), but the solution above is the simplest I can think of. BTW it is not really thread-safe. To really be thread-safe, every access to the boolean needs to be locked by a NSLock object from either thread (using such a lock also makes "volatile" obsolete, as variables protected by a lock are implicit volatile according to POSIX standard; the C standard however doesn't know about locks, so here only volatile can guarantee this code to work; GCC doesn't need volatile to be set for a variable protected by locks).

Mecki
Very cool. This adds a new tool to my objc arsenal. Consider adding your techique to [this thread][1] [1]: http://stackoverflow.com/questions/155964/what-are-best-practices-that-you-use-when-writing-objective-c-and-cocoa#156343
schwa
+5  A: 

In general, if you are processing events yourself in a loop, you're Doing It Wrong. It can cause a ton of messy problems, in my experience.

If you want to run modally -- for example, showing a progress panel -- run modally! Go ahead and use the NSApplication methods, run modally for the progress sheet, then stop the modal when the load is done. See the Apple documentation, for example http://developer.apple.com/documentation/Cocoa/Conceptual/WinPanel/Concepts/UsingModalWindows.html .

If you just want a view to be up for the duration of your load, but you don't want it to be modal (eg, you want other views to be able to respond to events), then you should do something much simpler. For instance, you could do this:

- (IBAction)start:(id)sender
{
    pageStillLoading = YES;
    [NSThread detachNewThreadSelector:@selector(runInBackground:) toTarget:self withObject:nil];
    [progress setHidden:NO];
}

- (void)wakeUpMainThreadRunloop:(id)arg
{
    [progress setHidden:YES];
}

And you're done. No need to keep control of the run loop!

-Wil

Wil Shipley