Search code examples
iosswiftmultithreadinggrand-central-dispatch

Swift: Safe Thread using NSLock or Concurrent Queue


What is the best way to do Safe Thread?

Using NSLock:

class Observable<T> {

    typealias Observer = (_ observable: Observable<T>, T) -> Void
    
    private var observers: [Observer]
    private let lock = NSLock()
    private var _value: T

    var value: T {
        get {
            lock.lock()
            let value = _value
            lock.unlock()
            return value
        }
        set {
            lock.lock()
            _value = newValue
            lock.unlock()
        }
    }

    
    init(_ value: T) {
        self._value = value
        observers = []
    }

    func observe(observer: @escaping Observer) {
        self.observers.append((observer))
    }

    private func notifyObservers(_ value: T) {
        DispatchQueue.main.async {
            self.observers.forEach { [unowned self](observer) in
                observer(self, value)
            }
        }
    }

}

Using Queue:

class SecondObservable<T> {

    typealias Observer = (_ observable: SecondObservable<T>, T) -> Void
    
    private var observers: [Observer]
    private let safeQueue = DispatchQueue(label: "com.observable.value", attributes: .concurrent)
    private var _value: T

    var value: T {
        get {
            var value: T!
            safeQueue.sync { value = _value }
            return value
        }
        set {
            safeQueue.async(flags: .barrier) { self._value = newValue }
        }
    }

    
    init(_ value: T) {
        self._value = value
        observers = []
    }

    func observe(observer: @escaping Observer) {
        self.observers.append((observer))
    }

    private func notifyObservers(_ value: T) {
        DispatchQueue.main.async {
            self.observers.forEach { [unowned self](observer) in
                observer(self, value)
            }
        }
    }

}

Or serial Queue:

class ThirdObservable<T> {

    typealias Observer = (_ observable: ThirdObservable<T>, T) -> Void
    
    private var observers: [Observer]
    private let safeQueue = DispatchQueue(label: "com.observable.value")
    private var _value: T

    var value: T {
        get {
            var value: T!
            safeQueue.async { value = self._value }
            return value
        }
        set {
            safeQueue.async { self._value = newValue }
        }
    }

    
    init(_ value: T) {
        self._value = value
        observers = []
    }

    func observe(observer: @escaping Observer) {
        self.observers.append((observer))
    }

    private func notifyObservers(_ value: T) {
        DispatchQueue.main.async {
            self.observers.forEach { [unowned self](observer) in
                observer(self, value)
            }
        }
    }

}

NSLock or a Queue with .concurrent attribute for the above case, and why?


Solution

  • Concurrent queue with barrier flag is more efficient than using NSLock in this case.

    Both of them block other operations while a setter is running but the difference is when you call multiple getters concurrently or parallelism.

    • NSLock: Only allow 1 getter running at a time
    • Concurrent Queue with barrier flag: Allow multiple getters running at a time.