2014-09-17

Here’s a subject that’s rarely discussed amond developers, even though it’s one of the most important component of all apps: Run Loops. Run loops are like the beating heart of apps; they are what make your code actually run.

The basic principle of a run loop, in fact, is quite simple. On iOS and OS X 1, CFRunLoop implements the core mechanism used by all the higher-level messaging and dispatching APIs.

What’s a Run Loop Anyway?

Put simply, a run loop is a messaging mechanism, used for asynchronous or interthread communication. It can be seen as a post box that waits for messages and delivers them to recipients.

A run loop does two things:

  • wait until something happens (e.g., a message arrives),
  • dispatch that message to its receiver.

On other platforms2, this mechanism is called the “Message Pump”.

Run loops are what separates interactive apps from command-line tools. Command-line tools are launched with parameters, execute their command, then exit. Interactive apps wait for user input, react, then resume waiting. In fact, this basic mechanism is found in pretty much every long-lived process. A good old while(1){select();}3 in a server is a run loop.

The job of a run loop is to wait for things to happen. Those things can be external events, caused by the user or the system (e.g. a network request.) or internal app messages, like inter-thread notifications, asynchronous code execution, timers… Once an event (or a “message”) is received, the run loop finds a relevant listener and pass it the message.

A basic run loop is actually fairly simple to implement. Here’s a trivial pseudocode version:

func postMessage(runloop, message)
{
    runloop.queue.pushBack(message)
    runloop.signal()
}

func run(runloop)
{
    do {
        runloop.wait()
        message = runloop.queue.popFront()
        dispatch(message)
    } while(true)
}

With this simple mechanism, each thread will run() its own run loop, and exchange messages with other threads’ run loops asynchronously using postMessage(). My colleague Cyril Mottier pointed me to the Android implementation which isn’t much more complex that that.

What about iOS and OS X

On Apple systems, that’s the job of CFRunLoop, in a slightly more advanced variant 4. All the code you write is called out by CFRunLoop at some point, except for early initialization, or if you spawn threads by yourself. (As far as I know, threads created automatically for Grand Central Dispatch queues don’t need a CFRunLoop, but certainly have a messaging system to allow reuse.)

The most important feature of CFRunLoop is the CFRunLoopModes. CFRunLoop works with a system of “Run Loop Sources”. Sources are registered on a run loop for one or several modes, and the run loop itself is made to run in a given mode. When an event arrives on a source, it is only handled by the run loop if the source mode matches the run loop current mode.

In addition, CFRunLoop can be reentered from the application code, either from your own code or within frameworks. As there is exactly one CFRunLoop for each thread, when a component wants to run it in a special mode, it simply calls CFRunLoopRunInMode(). All the run loop Sources that aren’t registered for this mode simply stop being served. Usually, that component eventually returns control to the previous mode.

CFRunLoop defines a pseudo-mode called the “common modes” (kCFRunLoopCommonModes), which is actually a set of modes containing the “normal” run loop modes for your app. The main run loop, at first, runs in kCFRunLoopCommonModes.

UIKit, on the other hand, defines a special run loop mode called UITrackingRunLoopMode. It uses this mode “while tracking in controls takes place” i.e. during touch events. This is very important, because this is what makes tableview scrolling smooth. While the main thread’s run loop is in UITrackingRunLoopMode, most background events, like network callbacks, aren’t delivered. This way, no extra processing is done, and there’s no lag in scrolling. (Well at least, now it‘s your fault.) 5

CFRunLoop demystified

If you’ve already debugged an iOS on OS X stack trace, you’ve probably noticed, down in stack trace, an all caps message starting with CFRUNLOOP_IS_CALLING_OUT. When CFRunLoop calls out to application code, it likes to make a show about it. There are six such functions, defined in CFRunLoop.c:

static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();

As you can guess, these functions have no purpose except helping debugging in the stack trace. CFRunLoop makes sure that all application code is called through one of these functions.

Let’s take a look at them one by one.

static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(
    CFRunLoopObserverCallBack func,
    CFRunLoopObserverRef observer,
    CFRunLoopActivity activity,
    void *info);

Observers are a bit special. The CFRunLoopObserver API lets you observe the behavior of CFRunLoop and be notified of its activity: when it processes events, when it goes to sleep, etc. It’s mostly useful for debugging, and you probably won’t need it in your app, but it’s there if you want to experiment with CFRunLoop’s features. [Update 2014-10-02: In fact, there are certain purposes where it will be useful: for example, CoreAnimation runs from an observer callout. It makes sense: by making sure all the UI code has run, it executes all the animations at once.]

static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(
    void (^block)(void));

Blocks are the flipside of the CFRunLoopPerformBlock() API, which is useful when you want to run code “on the next loop”.

static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(
    void *msg);

That Main Dispatch Queue tag is of course CFRunLoop speaking to Grand Central Dispatch. Obviously, on the main thread at least, GCD and CFRunLoop work hand in hand. Even though GCD can (and will) create threads without a CFRunLoop, when there is one, it will plug itself in.6

static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(
    CFRunLoopTimerCallBack func,
    CFRunLoopTimerRef timer,
    void *info);

Timers are relatively self-explained. On iOS and OS X, high-lever “timers” like NSTimer or performSelector:afterDelay: are implementd using CFRunLoop timers. Since iOS 7 and Mavericks, Timers can now have a tolerance on their fire date; this feature, too, is handled by CFRunLoop.

static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(
    void (*perform)(void *),
    void *info);

CFRunLoopSources “Version 0” and “Version 1” are actually very different beasts, even if they have a common API. Version 0 Sources are simply an in-app messaging mechanism and must be handled manually by the application code. After signaling a Version 0 Source (with CFRunLoopSourceSignal()), the CFRunLoop must be awaken (with CFRunLoopWakeUp()) for the source to be handled.

static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(
    void *(*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info),
    mach_msg_header_t *msg, CFIndex size, mach_msg_header_t **reply,
    void (*perform)(void *),
    void *info);

Version 1 Sources, on the other hand, handle kernel events using mach_ports. This is in fact the very heart of CFRunLoop: most of the time, when your app is standing there, doing nothing, it’s blocked in this single mach_msg(…,MACH_RCV_MSG,…) call. If you take a sample of any app with Activity Monitor, chances are you’ll something like this:

2718 CFRunLoopRunSpecific  (in CoreFoundation) + 296  [0x7fff98bb7cb8]
  2718 __CFRunLoopRun  (in CoreFoundation) + 1371  [0x7fff98bb845b]
    2718 __CFRunLoopServiceMachPort  (in CoreFoundation) + 212  [0x7fff98bb8f94]
      2718 mach_msg  (in libsystem_kernel.dylib) + 55  [0x7fff99cf469f]
        2718 mach_msg_trap  (in libsystem_kernel.dylib) + 10  [0x7fff99cf552e]

This is right here in CFRunLoop.c. Just a few lines above, Apple engineers commented with this very relevant quote from the Hamlet soliloquy:

    /* In that sleep of death what nightmares may come ... */

A sneak peek at CFRunLoop.c

Whenever your apps run, the core of CFRunLoop is the __CFRunLoopRun() function, called through the public APIs CFRunLoopRun() and CFRunLoopRunInMode(mode, seconds, returnAfterSourceHandled).

__CFRunLoopRun() will exit for four reasons:

  • kCFRunLoopRunTimedOut: on timeout, if an interval was specified,
  • kCFRunLoopRunFinished: if it becomes “empty”, e.g. all the Sources have been removed,
  • kCFRunLoopRunHandledSource: with the returnAfterSourceHandled flag, as soon as an event have been dispatched,
  • kCFRunLoopRunStopped: it has been stopped manually with CFRunLoopStop().

Until one of these reasons occur, it will keep on waiting and delivering events. Here’s a single pass looks like, handling the various event types we discussed above:

  1. Call “blocks”. (The CFRunLoopPerformBlock() API.)
  2. Check Version 0 Sources, and call out their “perform” function if necessary.
  3. Poll and internal dispatch queues and mach_ports, and
  4. Go to sleep if there’s nothing awaiting. The kernel will wake us up if something comes up. It’s actually a lot more complex in the code, because a/ the Win32 compatibility code adds a lot of #ifdef #elif noise and b/ there’s a goto right in the middle. The main idea is that mach_msg() can be configured to wait on several queues and ports. CFRunLoop does this to wait for timers, GCD dispatches, manual wakeups, or Version 1 Sources, all at the same time.
  5. Wake up, and try to see why:
    1. A manual wakeup. Just continue running the loop, maybe there’s a block or a Version 0 Source to serve.
    2. One or several Timers fired. Call their function.
    3. GCD needs to work. Call it via a specific ”4CF” dispatch_queue API.
    4. A Version 1 Source has been signaled by the kernel. Find it and serve it.
  6. Call “blocks”, again.
  7. Check for exit conditions. (Finished, Stopped, TimedOut, HandledSource)
  8. Start all over again.

Whew. Easy, right? As you may know, CoreFoundation is implemented in C, and frankly doesn’t look too modern. Reading this, my first reaction was “Wow. This is begging for a refactor”. On the other hand, this code is more than field-tested, so I don’t expect a full rewrite in Swift anytime soon.

There’s a code pattern that I’ve been using a lot in recent years, especially in testing. It’s “Run the run loop until this condition becomes true”, the basis for any kind of asynchronous unit tests. Over time I’ve probably written a dozen of variants of this, using NSRunLoop or CFRunLoop directly, doing polling, using timeouts, etc. Now I may be able to write a decent version of it; let’s find out in the next post.

  1. We need a name for the “family of operating systems made by Apple“. “Darwin” wouldn’t work, as it is the name of the underlying system. OS? 

  2. In a previous life, I wrote my fair share of Win32 code. 

  3. man 2 select, if you’re lucky enough not to know. 

  4. CFRunLoop.c is a 3909 lines long, while Looper.java is just 309 lines. 

  5. You may remember from 2011 this post attributing the scrolling performance on iOS to a “dedicated UI thread with real-time priority”. It’s been corrected and clarifed later; of course, that was just UITrackingRunLoopMode doing its job. 

  6. To be honest, I’m not really familiar with the internals of GCD. I’m trying to guess how this can work, please correct me if I’m wrong.