Search code examples
iosswiftios-multithreadingnslock

Usage of NSLock in property setter


Let's say, there is a variable that I want to make thread safe. One of the most common ways to do this:

var value: A {
    get { return queue.sync { self._value } }
    set { queue.sync { self._value = newValue } }
}

However, this property is not completely thread safe if we change the value as in the example below:

Class.value += 1

So my question is: Using NSLock on the same principle is also not completely thread safe?

var value: A {
    get { 
       lock.lock()
       defer { lock.unlock() }
       return self._value
    }
    set { 
       lock.lock()
       defer { lock.unlock() }
       self._value = newValue
    }
}

Solution

  • That's interesting, I'm learning about this for the first time.

    The issue in the first bit of code, is that:

    object.value += 1

    has the same semantics as

    object.value = object.value + 1

    which we can further expand to:

    let originalValue = queue.sync { object._value }
    let newValue = origiinalValue + 1
    queue.sync { self._value = newValue }
    

    Expanding it so makes it clear that the synchronization of the getter and setter work fine, but they're not synchronized as a whole. A context switch in the middle of the code above could cause _value to be mutated by another thread, without newValue reflecting the change.

    Using a lock would have the exact same problem. It would expand to:

    lock.lock()
    let originalValue = object._value
    lock.unlock()
    
    let newValue = originalValue + 1
    
    lock.lock()
    object._value = newValue
    lock.unlock()
    

    You can see this for yourself by instrumenting your code with some logging statements, which show that the mutation isn't fully covered by the lock:

    class C {
        var lock = NSLock()
    
        var _value: Int
        var value: Int {
            get {
                print("value.get start")
                print("lock.lock()")
                lock.lock()
                defer {
                    print("lock.unlock()")
                    lock.unlock()
                    print("value.get end")
                }
                print("getting self._value")
                return self._value
            }
            set { 
                print("\n\n\nvalue.set start")
                lock.lock()
                print("lock.lock()")
                defer {
                    print("lock.unlock()")
                    lock.unlock()
                    print("value.set end")
                }
                print("setting self._value")
                self._value = newValue
            }
        }
    
        init(_ value: Int) { self._value = value }
    }
    
    let object = C(0)
    object.value += 1