Thread Safe Singleton
There are two thread safety cases to consider
- during initialization of the singleton instance
- during reads and writes to the instance (internal variables)
Initialization
In objective-c, we can use dispatch_once to ensure that it’s only created once.
+ (id)sharedManager {
static MyManager *sharedMyManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedMyManager = [[self alloc] init];
});
return sharedMyManager;
}
Global variables are guaranteed to be initialized in an atomic fashion.
Define a singleton
private let _sharedManager = PhotoManager()
class PhotoManager {
class var sharedManager: PhotoManager {
return _sharedManager
}
}
class NetworkManager {
static let shared = NetworkManager(baseURL: API.baseURL)
let baseURL: URL
private init(baseURL: URL) {
self.baseURL = baseURL
}
}
class NetworkManager {
private static var sharedNetworkManager = NetworkManager()
let baseURL: URL
// Initialization
private init(baseURL: URL) {
self.baseURL = baseURL
}
// MARK: - Accessors
class func shared() -> NetworkManager {
return sharedNetworkManager
}
}
Readers-Writers Problem
- Any variable declared with the let keyword is considered thread-safe
- Any variable declared using var, and mutable collection types are thread-unsafe
GCD provides an elegant solution of creating a read/write lock using dispatch barriers. Dispatch barriers are a group of functions acting as a serial-style bottleneck when working with concurrent queues.
- Don't use barriers in global queues as these queues are shared resources.
- Using barriers in a custom serial queue is redundant as it already executes serially.
- Using barriers in custom concurrent queue is a great choice for handling thread safety in atomic of critical areas of code
fileprivate let concurrentQueue = DispatchQueue(label: "com.mydoc.photoQueue", attributes: .concurrent)
// Getter
var photos: [Photo] {
var photosCopy: [Photo]!
concurrentPhotoQueue.sync {
photosCopy = self.photos
}
return photosCopy
}
// Setter
func addPhoto(_ photo: Photo) {
concurrentQueue.async(flags: .barrier) { [weak self] in
guard let strongSelf = self else { return }
storngSelf.photos.append(photo)
}
}
The Deallocation Problem
- UI objects should be deallocated on the main thread
- Since it’s common that a secondary thread, operation, or block retains the caller, this is very easy to get wrong and quite hard to find/fix.
Consistent use of weak and not accessing ivars in async blocks/operations helps.