Working with Multiple Threads

GraphPaper is a tricky application because it has to do three things at the same time:

  1. Respond to user events.

  2. “Listen” for data from Evaluator and graph it when it arrives.

  3. Send data to Evaluator.

Handling (i) and (ii) at the same time is no problem: we saw how to do that in the MathPaper application in Chapter 11. The NSApplication object’s event loop, which watches for user events, will also watch for data on a file descriptor[38] using the NSTask, NSPipe, and NSFileHandle classes. The problem is (iii) — sending data to the Evaluator process. Doing this concurrently with (i) and (ii) presents a problem in Cocoa that has to do with the way that operating systems handle pipes.

Unix Pipes and Evaluator

When two programs are connected with a Unix pipe , the operating system allocates a buffer to address the possibility that the program at the “write end” of the pipe might send data before the program at the “read end” of the pipe is ready to accept it. Of course, each pipe buffer is only so big, and thus if the program at the read end of the pipe doesn’t read the data fast enough, the pipe buffer gets filled. If the pipe buffer is filled and the program that is writing tries to keep sending data down the pipe, that program will be blocked until the pipe buffer has some empty space.

It is much faster to send data to Evaluator than it is for Evaluator to process the data and send it back, so it’s reasonable to assume that any process sending data to Evaluator through a pipe will eventually be blocked. In addition, Evaluator will send results data through another pipe back to the same process from which the data came. That second pipe can fill up just as easily as the pipe that sends data to Evaluator. This can result in a deadlock condition , with both pipes filled and both processes blocked, each waiting for the other to empty the pipe from which it is reading. In our example, the main GraphPaper process would be blocked because Evaluator couldn’t accept any more data, and Evaluator would be blocked because the GraphPaper application wasn’t emptying its pipe either. The user would see one of those never-ending spinning disks indicating that the GraphPaper application had hung — a very undesirable result.

One way to solve this problem would be to have GraphPaper and Evaluator work in a lock-step fashion: GraphPaper could send a single line to Evaluator, then wait for that line to be returned. Many programmers take this approach, but you shouldn’t. Forcing two programs to run in lock-step invariably makes them both run very slowly, because the operating system needs to constantly switch between the two of them.

A far better approach is to let the GraphPaper process fill up the pipe and then go on to other tasks, such as accepting user input and emptying the pipe of data from the Evaluator process. While GraphPaper may be blocked because the pipe buffer directing data to Evaluator is full, the operating system will allow the Evaluator process to run. It will run as fast as it can, processing data from its input and writing the data to its output. The operating system will allow the Evaluator process to run in blocks, perhaps because its output buffer is filled, or until it has used up the maximum amount of CPU time that a process may use before the operating system forces a context switch. The GraphPaper process will then start up and start reading data returned from Evaluator.

This is the approach we will follow. Because there is no easy way for a process to see if writing to a pipe will block, our solution is to use a third execution thread — one that has only the job of sending data to Evaluator. In GraphPaper, we will call this process the stuffer . When Evaluator gets busy and the pipe buffer gets filled, the stuffer thread blocks. Because all the stuffer thread does is send data to Evaluator, it doesn’t matter if it gets blocked temporarily, because no blocked process will be waiting for the stuffer.

Threads

Although we could send the data to Evaluator with a completely different process using another NSTask object, a far more elegant (and efficient) way to do it is with a lightweight process called a thread . Simply put, a thread is another process that shares the same program and data space with the program that created it. A thread can access the same global variables as its creator, but it runs on its own schedule and can lock its own resources. Threads also have their own stacks, local variables, and, in Cocoa, their own autorelease pools. If you have a computer with two processors, multiple threads can run at the same time, each on its own CPU. Threads and multithread programming are an important part of Cocoa.

The power of threads does not come without a price — it’s harder to write a multithreaded application than to write a single-threaded one, because two processes executing in the same address space can cause adverse interactions. Programmers must be careful to anticipate and avoid such interactions — for example, some kinds of global variables must be locked every time they are used, to prevent accidental modification by another thread.

To see how such an interaction could happen, consider the following simple example. Suppose a function in a multithreaded program wanted to increment a global variable called count. In a single-threaded program, you would use an expression like this:

extern int count;
count = count + 1;

This simple increment operation might cause problems in a multithreaded application. Suppose that one thread read the value of count from memory, but before it could increment count and write the value back to memory, that thread was suspended and a second thread started up. Suppose that the second thread also read the value of count from memory (the same location) and incremented it. The second thread would have read the old, unincremented value of count from memory and incremented that value. Regardless of the order in which the threads write their values back to memory, the resulting value of count will be increased by only 1 (instead of by 2) when both threads are finished. A bug!

Locking with NSLock

The way around this problem is to use a mutually exclusive (mutex) lock . All programming environments that provide for multiple threads support some kind of locking system. In Cocoa, locks are implemented with the NSLock, NSConditionLock, and NSRecursiveLock Foundation classes.

Using these locking classes is quite simple. To implement an interthread variable called count, for example, we could create and initialize an NSLock object as follows:

int count = 0;
NSLock *countLock = [ [NSLock alloc] init];

Then we could increment the count variable in a thread as follows:

extern int count;
extern NSLock countLock;

[countLock lock];
count++;
[countLock unlock;]

If a second thread attempts to lock the countLock while the first thread has it locked, the second thread halts execution until the countLock is unlocked. This prevents two threads from simultaneously trying to access and modify the value of the variable count.

It’s obviously more work to write an application that uses multiple threads, and these applications are also dramatically harder to debug. Applications that are multithreaded also have somewhat more overhead than nonmultithreaded applications, because of the need to lock and unlock. For these reasons, the original NeXTSTEP Foundation and Application Kit were not multithreaded.

In recent years, Apple has worked to make Cocoa multithreaded. Today the Objective-C runtime and the Foundation and Application Kits are largely multithreaded. But the multithreaded implementation is not perfect. This means that if you write a multithreaded application, you should send messages to AppKit objects only from your application’s main thread. Before you write your own multithreaded application, you should also review the Apple documentation entitled “Overview of Programming Topic: Multithreading.” This document includes several sections, including:

“Threads”

Describes what threads are and how they are used

“Thread Safety”

Describes problems that can arise when using multiple threads

“Locks”

Describes the Cocoa locking system

Don’t let this discussion scare you off from writing multithreaded applications. These applications can be a lot of fun to write and debug. You will find your job considerably easier, however, if you restrict your use of Cocoa’s Application Kit objects to a single thread — the application’s main thread. Although the Application Kit is not fully multithreaded, that doesn’t mean that you shouldn’t use multiple threads — just don’t use them to access the defaults system or update the screen. It’s not a good idea to make users of your application wait for CPU-intensive processes to finish when they could be doing other useful things with your application. For this reason, programmers usually use threads for performing time-intensive tasks to be done in the background, so they won’t interfere with your main program’s handling of events.

Launching Threads with NSThread

Every Mac OS X application has at least one thread, called the main thread . The main thread is responsible for processing events and performs the primary communication with the Window Server. If you want to create a second thread, you use the NSThread class. This class is surprisingly simple; its most important methods are described in Table 16-1.

Table 16-1. Important methods in the NSThread class

Method

Purpose

+ (void)detachNewThreadSelector: (SEL)aSelector toTarget:(id)aTarget withObject:(id)anArgument

Creates a new thread. The thread starts up by sending the selector aSelector with the argument anArgument to the target aTarget. When the method returns, the thread dies.

+ (void)exit

Terminates the current thread.

+ (BOOL)isMultiThreaded

Returns true if the application is multithreaded — that is, if the application has executed the detachNewThreadSelector:toTarget:withObject method.

+ (void)sleepUntilDate: (NSDate *)aDate

Pauses the current thread until aDate.

Threads can communicate through TCP/IP connections, through NSPipes, by using shared memory, and by using Cocoa’s distributed object system. However, they cannot communicate via normal Objective-C messages. Although threads share the same address space, they are truly independent processes — each is separately scheduled and separately controllable. As such, there is no easy way for one thread to terminate another thread, although you can have one thread send another thread a message that causes the second thread to terminate itself when it reads and processes the message.



[38] File descriptors are also called file handles . They are the small integers that are returned by the Unix open( ) system function and are used by the read( ), write( ) , and close( ) functions.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.151.220