Main Thread Only APIs on OS X
A few weeks ago, Mike Ash wrote up a fantastic article about thread safety on OS X. Apple’s pushing technologies like NSOperation
, currently available on 10.5, and Grand Central, available in the upcoming 10.6 release, in order to help developers utilize all CPU cores and ease the pains of threaded programming, so thread safety is an important topic. One of the key points to his article was the difference between “not thread safe” and “main thread only,” and I think this topic deserves a closer look.
There are two problems with the current state of affairs. First, the documentation is poor. Apple’s API documentation often does not distinguish between “not thread safe” and “main thread only.” The second is that many classes, documented or not, are only safe to use from the main thread.
Both of these problems are important because knowing this information impacts how you write your code. The result is that your architecture is limited to main thread only. Main thread only complicates your code, plus it means you don’t get to use NSOperation
or Grand Central at all or to their fullest extent.
Before I delve into some more concrete examples of how the current situation affects architecture, I’d like to discuss why you would ever use multiple threads.
Why Multithreading
The first benefit to using multiple threads is that it allows you to spread CPU intensive tasks over multiple cores. Such CPU intensive tasks are include encoding audio, video, and generic number crunching for scientific applications.
However, even if your application is not CPU intensive (and most aren’t), multithreaded programming also helps with perceived responsiveness. You see, AppKit (the framework for building desktop Mac OS X applications) is not thread safe. It has the concept of a “main thread” where all user interface code runs. All drawing and event handling happens on this main thread. As a result, applications must not perform long running processes in response to user interactions.
For example, if a user clicks a button, and, in response, you perform a long running task, you will starve the main thread, blocking it from running. If your application locks up the main thread for about 2 to 4 seconds, your users will see what’s officially known as the spinning wait cursor, or unofficially named the “spinning pizza of death” (SPOD) or “the beach ball of hell”.
Regardless of what you call spinning wait cursor (I like SPOD), avoiding SPODs is essential to providing a good user experience. Keeping responses to user interactions fast means your application will feel snappy and quick.
Threading using Synchronous APIs
As I alluded to earlier, one way to to keep the main thread responsive is to offload long running tasks to a background thread. (Timers are another way, but that’s another post.) For example, let’s say we’ve got a task that’s composed of two steps, or subtasks. We could create a method such as this, that gets executed in its own thread:
- (void)performLongRunningTask
{
[self performTask1];
[self performTask2];
[self sendResultsToMainThread];
}
This code is pretty straight forward. It’s even relatively easy to add more subtasks, conditional logic, and error checking. With the standard definition of “not thread safe” you’d think this code is fine. All classes are used within the context of a single thread, thus they’re safe to use.
Now we see the problem of “not thread safe” versus “main thread only.” If these classes are main thread only, we’ve got a serious problem. We can’t use the technique of using a background thread to offload long running operations. This also eliminates using NSOperation
, in its default mode.
Coding on the Main Thread
Luckily, writing code only on the main thread without SPODs is sometimes possible. Say the classes we use for our two tasks are main thread only, but they also have asynchronous APIs. By asynchronous APIs, I mean they have methods that start a long running operation and send you some sort of notification when its finished . Many of Apple’s classes have asynchronous APIs, such as WebKit, PubSub, NSSpeechSynthesizer
, and many of the I/O classes. In order to write our long running task using asynchronous APIs on the main thread, we could use some sort of state machine:
- (void)startLongRunningTask
{
NSAssert(_state == kIdleState, @"incorrect state");
[self startTask1];
_state = kPerformingTask1State;
}
- (void)task1Finished
{
NSAssert(_state == kPerformingTask1State, @"incorrect state");
[self startTask2];
_state = kPerformingTask2State;
}
- (void)task2Finished
{
NSAssert(_state == kPerformingTask2State, @"incorrect state");
[self doSomethingWithResults];
_state = kIdleState;
}
As you can see, this reorganization complicates our long running task quite a bit. We’re forced to break up the flow of execution into multiple methods. Add in more than two steps, conditional logic flow, and error handling, and all of a sudden our state machine code gets even more complicated.
While there are techniques and tools to help write state machines, for example the state pattern and the state machine compiler, I’d rather do this kind of stuff using synchronous APIs. Synchronous code is easier to understand and write, thus its easier to avoid bugs and extend the code in the future.
Now if the APIs you want to use not only require the main-thread, but do not provide asynchronous methods, you’re in an even worse situation. You can’t move these long running tasks to a background thread and have to risk SPODing. Apple Script and Scripting Bridge are two APIs I’m now familiar with, having used them on Textcast, that have this limitiation.
The Bugs
Given the availability of NSOperation
and Grand Central, it’s only getting more important to know which classes we can and cannot use on background threads. I’ve opened two bugs regarding the documentation:
- r. 6522412: Thread safety should be explicitly mentioned in every classes’ API documentation
- r. 6522368: The Threading Programming Guide does not mention main-thread safety
Also, requiring so many classes to be on the main thread is very limiting. I don’t like my architecture to be backed into a corner. I’d rather not write state machines and I’d rather be able to use NSOperation
. Hopefully we’ll see more classes that are safe for background threads in Snow Leopard, but this is a much harder issue for Apple to tackle.
I should probably open a bug regarding Scripting Bridge being synchronous and main-thread only, as well. There are probably other APIs that also fall into this category, that I just haven’t personally run across yet.