Cocoa Programming for Mac OS X - Aaron Hillegass [128]
A Deep Chasm Opens Before You
While multithreading can be essential in certain applications, it also opens up an entirely new category of bugs that are notoriously difficult to fix: race conditions. It is for this reason that we suggest that you carefully weigh the benefits of multithreading against the significant costs.
Race conditions occur when code is modifying the same data in two or more threads. Consider the classic case of two threads incrementing a global variable (Figure 34.1). Here is the global integer variable:
int globalCount = 0;
Figure 34.1. A Classic Race Condition
Now imagine the two threads executing this code concurrently:
for (int i = 0; i < 1000; i++) {
globalCount = globalCount + 1;
}
It looks like the programmer’s expectation is that globalCount will be equal to 2,000 when both threads have completed. Depending on how the threads are scheduled, however, the final value of globalCount may be much less. Why is this? The reason is that the thread scheduler may interrupt any thread at any time in order to run another thread, or it may even run multiple threads simultaneously on the cores of a multicore system. So it becomes somewhat dangerous that the statement
globalCount = globalCount + 1;
is actually several instructions:
i. Load the value of globalCount into a CPU register.
ii. Add 1 to this value.
iii. Store the result back to globalCount.
Consider the effects of two threads running these instructions over and over. The incremented value of globalCount by one thread runs a very high risk of being clobbered by the other thread. Worse, the results of such code can be inconsistent between runs on different systems or even the same system.
This is a rather low-level example of a race condition, but it illustrates the general problem with multithreading: You cannot make any assumptions about when a thread will be scheduled, how long it will execute before being interrupted, what other threads might be running at the same time, or exclusivity as far as access to data. Usually, a race-condition bug will have much more serious implications than a not fully incremented integer.
Fortunately, Cocoa provides some tools for dealing with these problems. They won’t be solved magically, and you will need to use the tools with care, as they can create their own set of problems, such as deadlocks. We will examine one of these tools at the end of the chapter.
If you plan to use multithreading in your application, take some time early on to consider how your data structures will be used, and try to minimize any sharing of data structures. Careful design will save you a lot of headaches down the road when it comes time to debug.
Simple Cocoa Background Threads
Now that you are sufficiently wary of multithreading, let’s look at one way to create a background thread in Cocoa. NSThread.h has a very handy NSObject category with the following method:
- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg;
We can use this category method to create a background thread with just a selector:
- (void)buttonClicked:(id)sender
{
[self performSelectorInBackground:@selector(backgroundOperation:)
withObject:nil];
}
- (void)backgroundOperation:(id)unused
{
// do background work ...
// the thread will end once this method returns.
}
Easy, right? Behind the scenes, an NSThread instance is created that runs the selector on the receiver object (in this case, self).
Typically, it’s not much fun to do something in the background. More often than not, we would like to update the UI with the results of our background work. Remember, any updates to the UI must be made on the main thread; we can use another NSObject category method, performSelectorOnMainThread:withObject:waitUntilDone:, for that. Let’s flesh out our backgroundOperation: method a bit more:
- (void)buttonClicked:(id)sender
{
[self performSelectorInBackground:@selector(backgroundOperation:)
withObject:nil];
}
- (void)backgroundOperation:(id)unused