Singletons in Cocoa/Objective-C

I’ll preface this post with the standard advice: Don’t create singletons if you don’t absolutely have to. In general, if you’re creating a global “manager” object of some sort, you’re doing something wrong.

That said, there’s still occasionally a reason to have such a global singleton, such as a “default something.” The sample code in the Cocoa Fundamentals Guide goes to a lot more trouble than it needs to in order to ensure that a class is a singleton.

This is almost never what you want.

First off, you probably want your class to be testable in a variety of configurations. In your unit tests, instead of getting your shared singleton instance in your -setUp method and “resetting” its state in -tearDown, you’d be better off just instantiating a new instance in -setUp and releasing it in -tearDown.

Also, the example in the Cocoa Fundamentals Guide does a lot of work that it simply doesn’t need to. This is all you really need to do to create a singleton in Cocoa:

That’s it! The astute reader will notice, of course, that this isn’t thread-safe. I got rid of the @synchronized (self) because it won’t do the right thing; depending on what actual class is sent +sharedManager, the value of self will be different!

For the sake of argument, though, let’s say that you do want a singleton with which you can interact from multiple threads at once. One way to do this would be to create your singleton instance in +initialize since it will always be run, on a single thread, before any other methods in your class:

By doing this, you avoid the performance bottleneck of @synchronized taking a recursive lock every time +sharedManager is invoked.

If you want to get fancier, and it’s OK to temporarily have more than one instance of your singleton created, you could even use objc_atomicCompareAndSwapGlobalBarrier to assign the value to return from +sharedManager, though this is probably also more work than it’s worth; after all, +initialize will only be invoked once for your class. (Though it can be re-invoked as a side-effect of initializing subclasses, hence the if (self == [SomeManager class]) { } idiom.)

In all of the above cases, you’ve done a whole lot less work than the example in the Cocoa Fundamentals Guide, and your code is a lot more likely to be correct as a result.

correct me if I’m wrong, but there seems to be a typo in the very first example as the static declaration is inside the class method, which means it will always be set to nil, and thus not really work as a “somewhat” singleton. Is that correct?

There’s actually no way for +initialize to be called with a self that isn’t the class +initialize belongs to, by the runtime. The runtime calls initialize in reverse order for the entire superclass tree (assuming that class has not already been initialized) and does not call “[super initialize].” +initialize is called once and only once for every class, as noted in the Apple documentation.
The if(self == [SomeManager class]) guard is therefore unnecessary.

But your singleton isn’t a singleton at all – you can [[SomeManager alloc] init] as many instances as you like. The point of the Apple way which overrides allocWithZone: is to enforce singleton behaviour – if that’s not important to a singleton class, it maybe doesn’t need to be a singleton anyway.

I’ve run into a situation that demonstrates that the example published here is not actually thread safe.

Two threads call +sharedManager at (more or less) the same time. The earlier call to +sharedManager triggers +initialize on its thread, but before +initialize completes the second thread executes +sharedManager.

When +sharedManager is called on the second thread it can return nil if +initialize hasn’t finished yet on the first thread.

This is very very interesting. I’m in the situation that I want to create a global “manager” to manage Core Data (all the methods to access Core Data in the delegate make me really uncomfortable).
Now I’m wondering: what is the right thing to do if singleton is bad?

Post a Comment

Your email is never published nor shared. Required fields are marked *