DekGenius.com
[ Team LiB ] Previous Section Next Section

2.9 Threaded Programming

Cocoa uses the NSThread class to provide multiple threads of execution in applications. Threads are useful if you want to perform a computationally intensive or time-consuming procedure in the background while allowing the main thread of execution to continue normally. When executing code in the main thread of the application, the flow of execution must stop while that code completes execution. If code takes a long time to execute, the user will notice that the application user interface controls freeze and do not respond to any actions.

To correct this problem, you could isolate the time-consuming code into its own method, and then execute that method in its own thread. This is how NSThread works—using the detachNewThreadSelector:toTarget:withObject: method, as shown in Example 2-38.

Example 2-38. Multithreading an application
// This method is invoked in response to some user action
- (void)someActionMethod:(id)sender
{
    [NSThread detachNewThreadSelector:@selector(longCode)
                             toTarget:self withObject:nil];
}

- (void)longCode
{
    NSAutoreleasePool *pool;
    pool = [[NSAutoreleasePool alloc] init];
    BOOL keepGoing = YES;

    while ( keepGoing) {
        // Do something here that will eventually stop by
        // setting keepGoing to NO
    }

    [pool release];
}

As illustrated in Example 2-38, threads exit after the natural completion of a method. If the threaded method is to integrate properly with Cocoa objects, then that method is responsible for setting up and destroying its own autorelease pool.

You can also cause a thread to exit before the natural completion of a method's execution by invoking the exit method. In fact, this is the method NSThread uses to cause a thread to exit normally at the end of a method. For example, you could insert a conditional into your threaded processing loop, as shown in Example 2-39.

Example 2-39. Using NSThread's exit method
- (void)longCode
{
  NSAutoreleasePool *pool;
  pool = [[NSAutoreleasePool alloc] init];
  BOOL exitEarly = NO;

  while ( YES ) {
    // Your code here, which may set exitEarly = YES

    if ( exitEarly ) {
      [pool release];
      [NSThread exit];
    }

    [pool release];
}

NSThread also declares the sleepUntilDate: method, which instructs the thread to take a break until the indicated date is reached. This is demonstrated in Example 2-40.

Example 2-40. Using NSThread's sleepUntilDate: method
- (void)longCode
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    BOOL keepGoing = YES;

    while ( keepGoing ) {
        // Put the thread to sleep for 10 seconds
        NSDate *d = [NSDate dateWithTimeIntervalFromNow:10.0];
        [NSThread sleepUntilDate:d];

        // Do something here that will eventually stop by
        // setting keepGoing to NO
    }

    [pool release];
}

Another useful method of NSThread lets you specify the priority for a thread in the same sense that you can specify a priority for a process using the commands nice and renice. The method is setThreadPriority:, which takes as its argument a float between 0.0 and 1.0. A value of 1.0 gives the thread highest priority, while 0.0 gives the thread lowest priority.

Examples Example 2-38 through Example 2-40 indicate that when working with objects in a threaded method, you should always create and destroy an autorelease pool manually for that thread. NSApplication defines a thread convenience constructor, which detaches a thread for a specified method, managing the autorelease pool for that thread. This method, declared in NSApplication, is detachDrawingThread:toTarget:withObject:. The arguments used by this method are the same as NSThread's detachNewThreadSelector:toTarget:withObject:, discussed previously in this section.

2.9.1 Locks

One of the difficulties of making applications multithreaded is making certain that data accessible from multiple threads (such as class instance variables) is not accessed simultaneously by multiple threads. Cocoa provides a solution to this problem through the class NSLock, which provides a mechanism for coordinating the actions of multiple threads.

Foundation defines the NSLocking protocol and three classes that conform to this protocol: NSLock, NSRecursiveLock, and NSConditionLock. For a class to conform to the NSLocking protocol, it must implement the lock and unlock methods. The three classes that do conform to this protocol add methods of their own, as described later in this section.

How Locks Work

Before a thread can execute code that was protected by a lock, a thread must first acquire the lock. A thread acquires a lock by sending a lock message to an object that conforms to the NSLocking protocol. If a thread has acquired a lock and hasn't yet relinquished it, then any other threads wishing to acquire the lock must stop in their tracks until the current thread releases the lock. By setting a lock in a thread, you prevent other threads from executing a section of code until the lock is removed. Thus, you can prevent threads from simultaneously accessing data by either protecting a common access point to the data with a lock, or by protecting every part of the code that accesses a certain variable with the same lock. This can be done in as shown in Example 2-37. Note that locks are generally created before an application becomes multithreaded, not when the lock is about to be used.

Using locks is one way to make data sharing between threads safe. Using Cocoa's distributed objects system, discussed at the end of this chapter, is another way to transfer data between threads safely.

2.9.1.1 NSLock

NSLock is the simplest lock in the Foundation framework. Example 2-41 shows how it works.

Example 2-41. Using NSLock to coordinate threads
NSLock *aLock = [[NSLock alloc] init];    // Previously created

[aLock lock];                            // Set the lock
[anObject setSomeData:toThisData];
[aLock unlock];                          // Relinquish the lock

One shortcoming of the code fragment in Example 2-27 is that if one thread has acquired the lock, aLock, and hasn't yet relinquished it, then other threads attempting to execute this code will be stopped cold while they wait to acquire the lock. This behavior would be undesirable in many situations. Using NSLock's tryLock method is one potential solution. This method attempts to acquire a lock, but doesn't sit around waiting for it. If the lock is immediately available for acquisition, the lock is acquired and tryLock returns YES. If another thread has the lock at the time tryLock is invoked, NO is returned and execution resumes. Thus, you could base the following conditional execution of code on the ability for a thread to acquire a lock:

// Use the same aLock from Example 2-41

if ( [aLock tryLock] ) {
    // Run your code here
    [aLock unlock];
}
2.9.1.2 NSRecursiveLock

Sometimes, in sufficiently complex code that has portions protected by locks, a thread attempts to acquire a lock more than once. However, if the thread has already acquired a lock and attempts to do so again, that thread freezes while it waits for the lock to become free. This lock never becomes free, since the thread needs to relinquish it, but the thread is frozen waiting for the lock to become available, ad infinitum.

The solution to this problem is to use a lock that is an instance of the NSRecursiveLock class. This lock lets a thread acquire a single lock multiple times, thus saving itself from the deadlock that would result in the use of NSLock. Example 2-42 shows an example in which a single lock might be acquired more than once by the same thread; this would fail when using an NSLock, so use NSRecursiveLock.

Example 2-42. Using NSRecursiveLock when NSLock would deadlock a thread
id rLock = [[NSRecursiveLock alloc] init];
int i;

[rLock lock];

for ( i = 0; i < 10000; i++ ) {
// Perform some thread-worthy, time-consuming computations

    if ( someCondition == YES ) {
        [rLock lock];
// Do something here
        [rLock unlock];
    }
}
[rLock unlock];
2.9.1.3 NSConditionLock

The third Foundation class that conforms to the NSLocking protocol is the NSConditionLock class. NSConditionLock lets you assign an arbitrary condition when the lock is initialized using initWithCondition:; the condition is just an integer. To acquire the lock, the thread sends the lock either a lockWhenCondition: or a tryLockWhenCondition: message (which are analogous in behavior to lock and tryLock). If the condition passed in these messages is equal to the condition of the receiver lock, then the lock is acquired. Additionally, when relinquishing the lock, you can use the method unlockWithCondition: rather than unlock; this sets the condition of the receiver to the new condition. Example 2-43 shows how to employ NSConditionLock.

Example 2-43. Conditional locking with NSConditionLock
id cLock = [[NSConditionLock alloc] initWithCondition:1];

[cLock lock];
// Do something here whose state you want to record
// as a condition in the lock
[cLock unlockWithCondition:0];
    [ Team LiB ] Previous Section Next Section