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]; } } } // ... @endNothing 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 laterFollow 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:
Post a Comment