swiftthread-safetyios-multithreading

how do I capture a property in thread-safe way


My class A has a property of class B which can be reset:

class A1 {
    private var b = B(0)

    func changeB(i : Int) {
        b = B(i)
    }

    func testB(k : Int) -> Bool {
        return b.test(k)
    }
}

class B {
    private let b : Int;

    init(_ i : Int) {
        b = i
    }

    func test(_ k : Int) -> Bool {
        return b == k
    }
}

So far so good. If I want to use class A in multithreading scenario, I must add some synchronization mechanism, because properties in Swift are not atomic by themselves:

class A2 {
    private var b = B(0)
    private let lock = NSLock()

    func changeB(i : Int) {
        lock.lock()
        defer { lock.unlock() }
        b = B(i)
    }
    
    func testB(k : Int) -> Bool {
        lock.lock()
        defer { lock.unlock() }
        return b.test(k)
    }

}

But now I want to introduce a closure:

class A3 {
    func listenToB() {
        NotificationCenter.default.addObserver(forName: Notification.Name("B"), object: nil, queue: nil) { 
        [b] (notification) in
            let k = notification.userInfo!["k"] as! Int
            print(b.test(k))
        }
    }
}

Do I understand correctly that this is not thread-safe? Will this get fixed if I capture lock as well, as below?

class A4 {
    func listenToB() {
        NotificationCenter.default.addObserver(forName: Notification.Name("B"), object: nil, queue: nil) { 
        [lock, b] (notification) in
            let k = notification.userInfo!["k"] as! Int
            lock.lock()
            defer { lock.unlock() }
            print(b.test(k))
        }
    }
}

Solution

  • Yes, using the captured lock ensures that the observer’s closure is synchronized with other tasks using the same lock. You can use this capturing pattern because lock happens to be a constant.

    That raises the more fundamental problem, namely the capturing of the b reference, which is not constant. That means that if you call changeB at some intervening point in time, your notification block will still reference the original captured B, not the new one.

    So, you really want to fall back to the weak self pattern if you want this to reference the current B:

    class A {
        func listenToB() {
            NotificationCenter.default.addObserver(forName: Notification.Name("B"), object: nil, queue: nil) { [weak self] notification in
                guard let self = self else { return }
    
                let k = notification.userInfo!["k"] as! Int
                self.lock.lock()
                defer { self.lock.unlock() }
                print(self.b.test(k))
            }
        }
    }