Please note: this article is part of the older "Objective-C era" on Cocoa with Love. I don't keep these articles up-to-date; please be wary of broken code or potentially out-of-date information. Read "A new era for Cocoa with Love" for more.
As pointed out by Mike Ash in his recent Friday Q&A 2009-11-13: Dangerous Cocoa Calls, NSHost
is not thread-safe for use outside of the main thread and due to potentially slow, synchronous network access is not really suitable for use on the main thread either. Fortunately, in Cocoa there are often ways to transparently fix classes that don't work as they should. In this post, I'll show you how you can transparently patch NSHost
using a drop-in solution and provide a non-blocking solution for NSHost
lookups.
NSHost
NSHost
is a class with a simple API that fetches the names or addresses of an internet host. You can use it to perform DNS lookups but one of the primary uses is to get the name and address of the current host.
All calls to NSHost
are synchronous — they block until the response is fetched. If a network error occurs, this could result in a 60 second delay before a timeout response occurs — definitely not something you want to do in your main thread.
Unfortunately, according to Cocoa's Thread Safety Summary, NSHost
is not thread-safe — so we can't simply pass the functionality off to another thread.
Does this mean you must revert to NSHost
's CoreFoundation equivalent, CFHost
, which is explicitly thread-safe? Not necessarily.
The problems we need to fix
It is not the NSHost
objects themselves that are the problem. NSHost
objects are immutable once allocated and immutable objects are implicitly thread-safe for most purposes.
The problem is the cache of NSHost
objects maintained internally by NSHost
when any of the lookups are called — access to this cache is unprotected from the perils of threading.
In addition to this, we need to be able to perform NSHost
lookups asynchronously.
Design of the solution
The key consideration in these changes will be a totally drop-in solution — NSHost
will immediately and transparently become thread-safe. No further code will be required.
The solution to the threading problem will be to create a corresponding category method for every class method of NSHost
which wraps all calls to NSHost
in a @synchronized
section and then in the load
method for the category, swizzle each of these corresponding methods into the place of the original method.
The asynchronous invocations can then be handled like any other asynchronous operation — by spawning a new thread which will call back when complete.
Swizzling alternate implementations
If you don't know what I meant by "swizzle", what we need to do is replace the existing implementations of the NSHost
class methods with our own implementations. The code for doing this is as follows:
static void SwizzleClassMethods(Class class, SEL firstSelector, SEL secondSelector)
{
Method firstMethod = class_getClassMethod(class, firstSelector);
Method secondMethod = class_getClassMethod(class, secondSelector);
if (!firstMethod || !secondMethod)
{
NSLog(@"Unable to swizzle class methods for selectors %@ and %@ on class %@",
NSStringFromSelector(firstSelector),
NSStringFromSelector(secondSelector),
NSStringFromClass(class));
return;
}
method_exchangeImplementations(firstMethod, secondMethod);
}
Then, in the load
method for our category...
@implementation NSHost (ThreadSafety)
+ (void)load
{
SwizzleClassMethods(self, @selector(currentHost), @selector(threadSafeCurrentHost));
SwizzleClassMethods(self, @selector(hostWithName:), @selector(threadSafeHostWithName:));
SwizzleClassMethods(self, @selector(hostWithAddress:), @selector(threadSafeHostWithAddress:));
SwizzleClassMethods(self, @selector(isHostCacheEnabled), @selector(threadSafeIsHostCacheEnabled));
SwizzleClassMethods(self, @selector(setHostCacheEnabled:), @selector(threadSafeSetHostCacheEnabled:));
SwizzleClassMethods(self, @selector(flushHostCache), @selector(threadSafeFlushHostCache));
SwizzleClassMethods(self, @selector(_fixNSHostLeak), @selector(threadSafe_fixNSHostLeak));
}
// category continues...
What this does is swaps in our new implementations, (e.g. threadSafeCurrentHost
) in place of Apple's original implementation (e.g. currentHost
). Once this is done, any call to currentHost
will result in our new code getting executed. Similarly, the original code that we replaced is now reachable by calling threadSafeCurrentHost
.
The implementation of each of these thread-safe methods takes the form:
+ (id)threadSafeCurrentHost
{
@synchronized(self)
{
return [self threadSafeCurrentHost];
}
}
This may look like the method is just calling itself but remember, after swizzling, the call to threadSafeCurrentHost
will actually invoke the original currentHost
code. So this method is actually running the original code but inside a @synchronized
section to maintain thread safety.
Asynchronous lookup
The best way to perform an asynchronous lookup, now that NSHost
will work in a thread-safe manner, is simply to perform the lookup in an NSOperation
and have that operation call back when done.
To do this, the ThreadSafety category also adds the methods:
currentHostInBackgroundForReceiver:selector:
hostWithName:inBackgroundForReceiver:selector:
hostWithAddress:inBackgroundForReceiver:selector:
to perform lookups and call back when done. These methods take the following form:
+ (void)hostWithName:(NSString *)name
inBackgroundForReceiver:(id)receiver
selector:(SEL)receiverSelector
{
[[self hostLookupQueue]
addOperation:
[[HostLookupOperation alloc]
initWithReceiver:receiver
receiverSelector:receiverSelector
receivingThread:[NSThread currentThread]
lookupSelector:@selector(hostWithName:)
lookupParameter:name]];
}
and the implementation of the HostLookupOperation
's main
method is extremely simple:
- (void)main
{
[receiver
performSelector:receiverSelector
onThread:receivingThread
withObject:[NSHost performSelector:lookupSelector withObject:parameter]
waitUntilDone:NO];
}
Conclusion
You can download the complete code for NSHost+ThreadedAdditions
(3kB).
The main advantage of this approach shown here is that you only need to add the files to your project — you do not need to add or change any other code to make this work.
These additions provide reasonably good thread safety for NSHost
as they channel all use of the class through the thread-safe wrapping methods. The limitation to this is that Apple could add further methods in the future that circumvent the @synchonized
sections we've added and the thread safety would be breached until swizzled methods were added for these new methods.
On the immutability of NSHost
instances — technically, the private instance variables names
and addresses
of NSHost
are allocated mutable but experimentally, I have verified that they are never mutated (in fact, there are no methods on NSHost
that would do this). However, localizedName
, available in Mac OS X 10.6, uses data from outside NSHost
so might not be thread-safe.
In reality, you can avoid all of this code and simply use the CFHost
API to achieve the same benefits. This ThreadedAdditions
category for NSHost
is an effort to continue using the simpler API of NSHost
and at the same time, to demonstrate that just because Apple's implementation of something is not thread-safe in its internal implementation, doesn't mean you can't make it thread-safe in the greater context of your whole program.
Creating iPhone and Mac icons using Inkscape (Part 2 of 2)
Performance tests: Replacing Core Data Key Paths