Tuesday, June 8, 2010

Objective-C Tuesdays: writing a thread safe getter method

We've spent several weeks looking at how properties are defined and created. Last time we saw how to change the default getter and setter names and in the post before that we learned about atomic and nonatomic properties. Today we'll cover one final topic related to properties: how to write a thread safe getter method.

In general, it's better to let the compiler generate your getters and setters by using the @synthesize directive: it's less error-prone and makes your class definitions shorter and easier to read. Sometimes however, you need to do something out of the ordinary that @synthesize can't provide. We covered writing getter and setter methods in a previous post. Those examples work fine for single-threaded programs, and we looked at how atomic getters and setters are generated by the @synthesize directive. As is frequently the case with multithreaded code, there is a subtle gotcha that occurs when retain/release interacts with multiple threads.

Let's use a very simple Bookmark class for a hypothetical browser app as an example. We'll try to make Bookmark objects thread-safe so that the browser app can load preview images of boomarked web sites in a background thread while the user edits bookmarks in the main thread.
@interface Bookmark : NSObject {
  NSURL *url;
  // ...
}
@property (retain) NSURL *url;
// ...
@end


@implementation Bookmark

- (NSURL *)url {
  NSURL *theUrl;
  @synchronized(self) {
    theUrl = url;
  }
  return theUrl;
}

- (void)setUrl:(NSURL *)theUrl {
  @synchronized(self) {
    if (theUrl != url) {
      [url release];
      url = [theUrl retain];
    }
  }
}

// ...
@end
Nothing very surprising here. In the getter for url, the temporary variable theUrl holds the pointer to the NSURL object that the getter returns. The @synchronized block around the assignment theUrl = url, along with a matching @synchronized block in the setter, makes sure that the assignment is atomic.

Note that we use an explicit temporary variable in the getter, instead of simply doing this:
// WARNING: compiler complains about this
- (NSURL *)url {
  @synchronized(self) {
    return url;
  }
}
because the compiler complains about the return statement in the middle of the @synchronized block.

With this getter and setter, we can set the URL from one thread while getting it on another thread and never see an invalid value. There's still a thread safety issue here though. Let's suppose that the background thread is updating the thumbnails for our browser app while the user decides to edit a bookmark. Pseudo-code for these actions might flow like this:
// given Bookmark instance bookmark:

// background thread gets URL from bookmark
NSURL *thumbnailUrl = bookmark.url; // url is "example.com", retain count 1

// ... background thread preempted by main thread ...

                    // main thread sets new url value
                    bookmark.url = newUrl; // newUrl is "sample.com", retain count 1
                    // same as [bookmark setUrl:newUrl];
                    
                    // -setUrl: method called:
                    - (void)setUrl:(NSURL *)theUrl {
                      @synchronized(self) {
                        if (theUrl != url) {
                          [url release];         // "example.com" released
                                                 // retain count now 0, -dealloc called
                          url = [theUrl retain]; // "sample.com" retained
                                                 // retain count now 2
                        }
                      }
                    }

/// ... main thread preempted by background thread ...

// thumbnailUrl now points to a deallocated object
NSData *webPage = [NSData dataWithContentsOfURL:thumbnailUrl];
// a crash will happen sooner or later
Follow the retain counts of the old and new NSURL objects in the pseudo-code above. Even though the getter and setter for the url property are atomic, using the NSURL object returned by the getter isn't thread-safe since the setter can cause the object to be deallocated while it's being used by code in another thread.

In Cocoa and Cocoa Touch, when you receive an Objective-C object as a return value from a method, there is an implicit promise that the object will remain valid at least for the rest of the currently executing function or method.

But how does the Bookmark instance keep the original url value alive after the setter is called? By using the autorelease pool. Before returning the NSURL object from the getter, we make the autorelease pool a second owner of the object. If the Bookmark object then releases the NSURL for any reason, the autorelease pool will keep the NSURL around until it is drained. Since each thread has its own autorelease pool, we don't need to worry about objects we're using being deallocated in another thread.

Rewriting the getter to autorelease:
- (NSURL *)url {
  NSURL *theUrl;
  @synchronized(self) {
    theUrl = [[url retain] autorelease];
  }
  return theUrl;
}
Note that we called -retain before calling -autorelease. I think of -retain as adding an owner for an object, and -release as removing an owner. The -autorelease method transfers the current ownership to the autorelease pool, which will call -release as some later time. So -retain makes the Bookmark object an owner twice, then -autorelease transfers one ownership to the autorelease pool, giving us two owners of the NSURL object.

So now the pseudo-code for the two threads interacting looks like this:
// given Bookmark instance bookmark:

// background thread gets URL from bookmark
NSURL *url = bookmark.url; // url is "example.com", retain count 2
                           // 1 for bookmark, 1 for autorelease pool

// ... background thread preempted by main thread ...

                    // main thread sets new url value
                    bookmark.url = newUrl; // newUrl is "sample.com", retain count 1
                    // same as [bookmark setUrl:newUrl];
                    
                    // -setUrl: method called:
                    - (void)setUrl:(NSURL *)theUrl {
                      @synchronized(self) {
                        if (theUrl != url) {
                          [url release];         // "example.com" released
                                                 // retain count now 1
                          url = [theUrl retain]; // "sample.com" retained
                                                 // retain count now 2
                        }
                      }
                    }

/// ... main thread preempted by background thread ...

// url now owned only by autorelease pool, but that's okay
NSData *webPage = [NSData dataWithContentsOfURL:url];
When you use @synthesize to generate getters and setters, the compiler generates thread-safe getters like this for you. When writing your own getters, it's a good practice to always retain and autorelease any Objective-C object you returned. Even if your code is only single threaded, you can still hang yourself by trying to use an object returned by a getter after calling the corresponding getter:
NSURL *oldUrl = bookmark.url;

bookmark.url = newUrl; // same as [bookmark setUrl:newUrl]
// oldUrl could be invalid if getter doesn't autorelease

NSLog(@"Replaced old URL %@ with new URL %@", oldUrl, newUrl);
// log statement might cause a crash

Next time, a summary of variables in Objective-C and the start of a new topic: character strings.

No comments: