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.