41

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?

Ned Batchelder
  • 364,293
  • 75
  • 561
  • 662
Dave Verwer
  • 6,140
  • 5
  • 34
  • 30
  • I think that you misunderstood the example given in the documentation; the usage of this example code is when you want to terminate the execution of a runloop (not to get notified about flag change) https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSRunLoop_Class/Reference/Reference.html#//apple_ref/occ/instm/NSRunLoop/run – kernix Apr 18 '14 at 20:06

7 Answers7

37

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.

Christian Di Lorenzo
  • 3,562
  • 24
  • 33
schwa
  • 11,962
  • 14
  • 43
  • 54
17

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
  • 125,244
  • 33
  • 244
  • 253
  • 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 Oct 07 '08 at 14:54
  • Great technique, very elegant. – Nick Moore May 06 '11 at 14:06
  • I want to know if the UIButton(sender) will keep the UIControlStateSelected state for 5 seconds? – user501836 Nov 01 '12 at 07:41
  • 1
    @user501836 I have no idea what you are talking about? Is this a comment to my answer or a totally unrelated question? If it is a totally unrelated question, please post a new question to the page (click "Ask Question" button, upper right corner of this page) – Mecki Nov 05 '12 at 16:51
  • This is a comment, I have no question. I have try you solution, and find that, after I tap the UIButton, it will keep blue color(in selected state) even if my touch up already. So I want to check with you, do you have solution for this problem. – user501836 Nov 06 '12 at 09:56
  • @Mecki I understand your example, however, I want to clarify 2 things. 1) If I'm only interested to know that pageStillLoading has changed, the while (pageStillLoading) {} should suffice (without ` [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];`) Have you just added this to give an example of how to use this? 2) I think that this code is indeed thread safe as pageIsLoading is a primitive type: http://stackoverflow.com/a/12938418/314848 – kernix Apr 18 '14 at 19:59
  • 2
    @kernix If you make a `while loop` as you suggested, it will waste 100% CPU time of a core whenever it waits (which heats up the CPU a lot and also wastes plenty of battery power), thus it is horribly inefficient. And it will also delay page loading dramatically on single core systems, as while the `while loop` is waiting, it blocks the CPU core and prevents other threads from running on it for some time. In my solution the main thread sleeps most of the time and needs no CPU time at all while waiting, so the core can perform other tasks in the meantime or go into power safe mode. – Mecki Apr 19 '14 at 03:23
  • @Mecki cool, thanks. What do you say the about thread safe issue? – kernix Apr 19 '14 at 05:06
  • 1
    @kernix Setting a bool across threads is safe by itself meaning it will not have any unexpected side effects, but it is not safe in regards to any other operation, as compilers and CPUs are free to re-arrange operations, perform them out of order or delay their effects (e.g. delay memory writes). `a = 5; a++; done = true`. What is the value of `a` when `done` becomes `true`? You cannot say, because the CPU may as well perform `a++` **after** setting `done` to `true`. It is only guaranteed that `a++` is performed before the **current thread** is reading `a` again. – Mecki Apr 22 '14 at 12:00
  • 1
    @kernix So if thread A runs `a = 5; a++; done = true` and thread B reads `a` as soon as `done` becomes `true`, thread B may read `a` as 6, but it may also read it as 5 (`a++` not yet performed) or as any other value (not even `a = 5` has been performed yet). Only thread A is guaranteed to read `a` as 6. To make sure all other threads read `a` as 6, too, you need a *memory barrier* (search for that term on Goolge to learn more). Mutexes and locks are memory barriers, performing a selector on a different thread includes such a barrier as well. Thread programming is harder than most people think. – Mecki Apr 22 '14 at 12:12
  • @Mecki awesome stuff, thanks! Last question, what are the benefits of declaring the boolean here as volatile? – kernix Apr 22 '14 at 16:43
  • @Mecki and when would I'd prefer volatile over lock? – kernix Apr 22 '14 at 16:53
  • 1
    @kernix `volatile` has nothing to do with threading or thread-safety, `volatile int x` only tells the compiler "whenever I access `x`, be sure to read it (again) from memory because the value of `x` might change *at any time*". If I don't use `volatile` and write `x = 0; while (x == 0);` the compiler could make `x = 0; while (true);` out of it as it sees no code ever changing `x`, so how could it change and why wasting CPU time for reading the same value over and over again from memory? All that `volatile` does is disabling some aggressive compiler optimization (which is necessary at times). – Mecki Apr 22 '14 at 17:34
  • 1
    @kernix In most casese you shouldn't use `volatile`, it will only slow down your code for no benefit. Also no need to use it if you protect access with mutexes, locks or semaphores. The compiler is smart enough to not make assumptions across any of these, that means it always assumes that after a lock was obtained or released, the value of any non-stack variable may have changed and thus automatically rereads it from memory the next time it is accessed. – Mecki Apr 22 '14 at 17:39
11

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
  • 9,343
  • 35
  • 59
10

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
  • 54,380
  • 8
  • 73
  • 102
  • 1
    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 Oct 07 '08 at 05:11
5

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 webViewIsLoading 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
  • 125,244
  • 33
  • 244
  • 253
  • 1
    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 Oct 07 '08 at 05:10
2

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.

DShah
  • 9,768
  • 11
  • 71
  • 127
Jon Shea
  • 1,207
  • 9
  • 7
1

Your second example just work around as you poll to check input of the run loop within time interval 0.1.

Occasionally I find a solution for your first example:

BOOL shouldKeepRunning = YES;        // global
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning && [theRL runMode:NSRunLoopCommonModes beforeDate:[NSDate distantFuture]]);
Richard
  • 339
  • 3
  • 10