I’m trying to better understand run loops as they apply to Mac applications (NSRunLoop), but this could also be a more general question. The NSRunLoop documentation says:
…your code provides the
whileorforloop that drives the run loop. Within your loop, you use a run loop object to “run” the event-processing code that receives events and calls the installed handlers.
The docs have a code example like this:
BOOL shouldKeepRunning = YES;
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
So the code keeps calling the run loop until it’s decided that it should terminate. The -runMode:beforeDate: method “Runs the loop once, blocking for input in the specified mode until a given date.” And there is also a -run method, which “Puts the receiver into a permanent loop, during which time it processes data from all attached input sources.”
How is it possible that calling the run loop repeatedly (or calling -run which, it sounds like, does this itself) doesn’t consume CPU? A Cocoa application can sit idly in the background while its main run loop runs, and it will consume zero (or nearly zero) CPU time.
And within -runMode:beforeDate:, how can the run loop block until input is received or timers fire without polling and consuming CPU?
This was a good excuse to poke into some Mac OS X internals! Luckily the relevant parts of Core Foundation are open source.
The usual answer to “How can a program wait for X without taking up CPU?” is “The kernel did it.” In this case, running the run loop really just means telling the kernel what you’re waiting for, and then letting the kernel context-switch away. In this case, the run loop spends most of its time in
mach_msgwith the flagMACH_RCV_MSG, which really is just a system call into the kernel, which ends up scheduling some other thread to run. Eventually, something interesting happens, which means that a Mach message is sent to a Mach port, and the kernel wakes up the blocked thread and delivers the message. The programs sees this as themach_msgfunction returning.There are all kinds of under-the-hood ways to get a Mach message sent to you. For example, if you set up an
NSTimer, that will probably work its way down to themk_timer_armsystem call, which simply causes a Mach message to get sent somewhere after a certain amount of time. The run loop, then, is not a fancy infinite loop so much as it is a dispatcher and a mapping between the kernel and the Cocoa (or Core Foundation) frameworks.It should go without saying that if the kernel has suspended your thread because you’re waiting on a message, that you aren’t actually using any CPU time, which is why your application appears to be idle. Why bother with an infinite loop when you have a modern kernel?