Monday, May 10, 2010

PSA: Respect the Main Thread

I tweeted this earlier as a joke, but it's actually a very serious thing. I've had several jobs in the last year where I've been asked to look at and either suggest fixes or actually fix iPhone applications written by other developers. One thing that I've seen several times in code I've reviewed is something like this littered throughout the application:

[NSThreadsleepForTimeInterval:0.3];

This code causes the thread it's called from the do nothing for the specified length of time. Then I'll look through the code for some indication that the application is spawning threads. So far, in every case, the app hadn't spawned any threads, explicitly, or implicitly using NSOperationQueue. If you haven't detached any threads, and you call sleepForTimeInterval:, you are sleeping the application's main thread, and that is a very bad thing to do.

Usually, I see these sleepForTimeInterval: calls in applications that do some kind of asynchronous network communication. My guess is that the developers who wrote these applications came from Java, .Net, or some other language where network communication is commonly handled on a non-main thread. In those languages, it's not uncommon to see code that puts the network worker thread to sleep or into a loop to make it wait for a response and account for any potential lag time. It's not a great approach, but in these environments, it generally works.

In Cocoa and Cocoa Touch, however, unless you specifically spawn a thread and register your network communications with the spawned thread's run loop, your network communications using CFNetwork as well as any networking done using the built-in networking functionality that exists in many Cocoa classes all happens on the main thread.

Another related problem that I sometimes see is something like this:

- (IBAction)soSomething
{
NSData *data = [NSDatadataWithContentsOfURL:[NSURLurlWithString:@"http://foo.com/reallybigdatafile"]];
while(1)
{
// parse really big data file and break when done
}

}

This code has a few really bad things going on. It's important to keep in mind that IBAction methods always fire on the main thread. So, the first problem is the call to dataWithContentsOfURL:. This is what's called a blocking network operation, which means that calling this method will prevent any further code on this thread from executing until all the data has been received because the method will not return until that has happened. The same thing is true for all of the convenience constructors that contain withContentsOfURL: in their name. While you can probably get away with very limited use of these methods for very small pieces of data, you really should be only using asynchronous network calls in your apps. You can't predict how long network communications will take, even with tiny pieces of data. If the radios are powered down, EDGE connections can take several seconds to re-establish, which is long enough that your user will probably notice the freeze.

The second problem with this thread is the while loop. Small loops are often okay in action methods, but those that could potentially be large or where the length of the loop is simply impossible to know (which is typically the case with while (1) or while(TRUE) loops), you don't want to be doing the processing on the main thread.

Why All This is Bad


If you sleep the main thread, quite literally nothing can happen in your app except for background thread execution. If you haven't spawned any background threads, then pretty much nothing can happen at all. Your user interface will freeze up and your user won't be able to use the application. Buttons won't highlight when tapped and animations will freeze in place. But it's even worse than the interface freezing (which is certainly bad enough), all event processing and network communications will freeze up also. Anything that is handled by your application's main event loop simply doesn't happen while the main thread is asleep.

I have yet to see a situation where calling sleepForTimeInterval: on the main thread is a good idea.

If you perform a long-running operation on the main thread, your code will monopolize the main thread and the end result will be essentially the same as sleeping the main thread while your loop runs. No user interaction, no network communications, no animation.

If you want a good introduction on how to get stuff off your main thread, check out Dave Dribbin's blog post on concurrent operations. More iPhone 3 Development also devotes a full chapter to different types of concurrency.

No comments:

Post a Comment