Swift Solutions: Singleton

We continue our swifty design pattern series with the famous Singleton. It is one of the most controversial design patterns: It ensures an object can only be instantiated once, and cannot be duplicated or replaced. Given the nature of singletons, the pattern can only be applied to reference types (classes), as value types like structs are copied on assignment.

A Warning

I should caution you: many programmers argue that singletons cause more problems than they solve. Quite frankly, I find myself in agreement with them.

The problem lies in the global nature of singletons. It can be changed from anywhere in our program, and it becomes difficult to maintain control over it. For example, an object may refer to a singleton only to find that its property values have changed out from under its feet — unbeknownst to us, some other component in our app modified our singleton. It also makes testing difficult, since it may be desirable to instantiate a second mock-object to test with. These are just a few of the problems arising from this pattern.

Use Cases

Having said that, the pattern is still a tool that has its uses. Even Apple’s Cocoa framework uses singletons (albeit sparingly). As such, we will cover the Singleton pattern in this series, but I urge you to exercise restraint when considering its usage.

Implementation

We will implement a singleton that represents a real-life object: a printing device.

class Printer {
static let sharedInstance = Printer()
private init() { }
}

let printer = Printer.sharedInstance

Couple things to note:

  1. Our printer exposes a single instance of itself via a static property.
  2. The init() method is private.

We make the initializer private so no other object can create an instance of Printer. This means a singleton can only be created from within the singleton class itself. Now we have a bit of a dilemma: Only the singleton can create a singleton, and no other object can bring the first singleton into existence. How then is a singleton ever created?

Interestingly enough, we use a static property. This property is accessible by other objects, but still within the boundaries of the singleton class. When sharedInstance is referred to for the first time, we instantiate and assign our singleton to it. Subsequent referrals to sharedInstance return the reference to our previously constructed instance.

This pretty much sums up how a singleton should be implemented. You could stop here and it would be totally fine. However if you continue, you will learn how address a common pitfall and a neat GCD feature to remedy it :)


Thread Safety and Singletons

Since singletons are global, that means multiple objects are likely to be accessing and using them at any given time. This poses a problem, and we need to modify our printer class in order to illustrate.

class Printer {
// 1
private var jobQueue = [String: String]()
static let sharedInstance = Printer()

private init() { }

// 2
func set(value: String, for key: String) {
jobQueue[key] = value
}

// 3
func value(for key: String) -> String? {
return jobQueue[key]
}
}
  1. We create a private dictionary jobQueue, which functions as a queue of printing tasks. Each key represents a job title, and the value represents the text to be printed. Please note that a dictionary is not the best way to organize this data; this is purely demonstrative.
  2. We create a function that allows us to add a new value to our jobQueue dictionary.
  3. Conversely, we also create a function that allows us to read values for a given key in our jobQueue.

Some of you already see the problem with this setup: We have a single shared instance of this object in our app, and multiple components potentially reading and writing to jobQueue. Our implementation is not thread-safe!

One possible solution is to perform all reads and writes on a serial queue like so:

class Printer {
private var jobQueue = [String: String]()
static let sharedInstance = Printer()
// 1
private let serialQueue = DispatchQueue(label: "SerialQueue")

private init() { }

func set(value: String, for key: String) {
// 2
serialQueue.sync {
self.jobQueue[key] = value
}
}

func value(for key: String) -> String? {
// 3
var result: String?
serialQueue.sync {
result = jobQueue[key]
}
return result
}
}
  1. We create a serial queue so tasks can only be performed one at a time.
  2. We update set(value: for:) so it synchronously writes to the dictionary on our serial queue.
  3. We update value(for:) so that it retrieves values on our serial queue as well.

This technically solves the problem of having multiple objects modifying our jobQueue at the same time, but it is also inefficient. Every time a read or write is requested, we would have to wait until all existing tasks finish. This also means a caller that simply reads a value would block others from doing the same until it’s finished. This can be improved.

Optimizing The Singleton: Final Implementation

We can create a far more efficient singleton with just a few more modifications:

class Printer {
// 1
private let concurrentQueue = DispatchQueue(label: "ConcurrentQueue", attributes: .concurrent, target: nil)
private var queue = [String: String]()
static let sharedInstance = Printer()

private init() { }

func set(value: String, for key: String) {
// 2
concurrentQueue.async(flags: .barrier) {
self.jobQueue[key] = value
}
}

func value(for key: String) -> String? {
var result: String?
// 3
concurrentQueue.sync {
result = jobQueue[key]
}
return result
}
}
  1. We create a concurrent queue to run multiple tasks on.
  2. We asynchronously write changes to jobQueue, and we also use a barrier to make our queue temporarily act as a serial queue.
  3. We synchronously call value(for key:) to concurrently read values from jobQueue. This is done in order to assign the value to result before returning it. Note that in an app we would take the extra precaution of not making a synchronous call on our main queue, and use a completionHandler to get the return value instead.

This may be a bit confusing, so let me elaborate: we created a concurrent queue that allows for multiple tasks to be executed simultaneously. However, only read operations are done concurrently, since multiple objects reading from jobQueue does not pose a problem in our app.

We do, however, temporarily make our concurrent queue act as a serial queue when writing to jobQueue. We do this by using GCD barriers, which halts our writing-task’s execution until existing tasks on the queue to finish. Tasks added to the concurrent queue during a write are prevented from executing until the writing task is finished (hence the name “barrier!”)

I will repeat this because it is easy to miss: Barriers ensure that the submitted block is the only work item executed on the queue. All work items submitted prior to the dispatch barrier must finish before the block will execute. Subsequently submitted work items won’t start until the flagged block completes.

With that, we have a singleton that is both thread-safe and efficient!

Conclusion

I don’t want to sound like a broken record, but I think it bears repeating that Singletons are almost never the answer to a problem you face in your app. Despite that, it is a tool like any other, and I leave its usage to the builder’s discretion.

We also saw that singletons can be deceptively easy to screw up in a multi-threading environment. Even though my example cut corners for illustrative purposes, I still hope the main points came across clearly.

Our next post will cover the Bridge Pattern. Hope you’ll look forward to it!

Originally published at emanleet.com on July 15, 2017.